From 7188c456efd1a153b06991f6919711f913f2df1e Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:01:34 -0700 Subject: [PATCH 01/40] docs: add terraform solution design spec (CX-43) Co-Authored-By: Claude Sonnet 4.6 --- .../2026-09-02-terraform-solution-design.md | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-02-terraform-solution-design.md diff --git a/docs/superpowers/specs/2026-09-02-terraform-solution-design.md b/docs/superpowers/specs/2026-09-02-terraform-solution-design.md new file mode 100644 index 00000000..a47ab2eb --- /dev/null +++ b/docs/superpowers/specs/2026-09-02-terraform-solution-design.md @@ -0,0 +1,258 @@ +# Terraform Solution Design + +**Date:** 2026-09-02 +**Issue:** CX-43 +**Solution tag:** `terraform` + +## Overview + +A Cortex solution bundle that teaches customers how to manage their Cortex catalog using the [Cortex Terraform provider](https://github.com/cortexapps/terraform-provider-cortex). The solution is hands-on: `cortex solutions install -s terraform` runs a `setup.py` that executes a real `terraform apply`, creating entities in Cortex via Terraform — not via YAML import. + +The solution ships: +1. **Working Terraform files** (`_templates/terraform/`) — provider config, teams, domains, services, and a scorecard for Parts Unlimited, the fictional company from *The Phoenix Project*. +2. **Delta files** (`_templates/terraform-delta/`) — modified versions of select `.tf` files demonstrating an incremental update: new service, scorecard promotion, team member added. +3. **`setup.py`** — checks for Terraform CLI, copies files to a working directory, writes `terraform.tfvars`, runs `terraform init` + `terraform apply`. + +No `catalog/` or `scorecards/` YAML files. Terraform is the sole source of truth. + +--- + +## Parts Unlimited Org Model + +Modeled after Parts Unlimited, the fictional automotive parts retailer from *The Phoenix Project*. + +### Teams (4) + +| Tag | Name | Description | +|---|---|---| +| `team-development` | Development | Application development (Bill's team) | +| `team-operations` | IT Operations | Infrastructure and operations (Brent's domain) | +| `team-security` | Information Security | Security and compliance (John's team) | +| `team-qa` | Quality Assurance | Testing and QA | + +### Domains (2) + +| Tag | Name | Services | +|---|---|---| +| `domain-ecommerce` | E-Commerce | `phoenix`, `parts-catalog-api`, `payments-service` | +| `domain-supply-chain` | Supply Chain | `inventory-service`, `ordering-service`, `shipping-service` | + +### Services — Initial State (6) + +All services start at **Bronze** level on the Production Readiness scorecard. They have descriptions, owner teams, and git configured — but deliberately lack on-call, runbook links, and custom data, so the delta can show meaningful improvement. + +| Tag | Name | Owner | Bronze rules met | +|---|---|---|---| +| `phoenix` | The Phoenix Project | `team-development` | description, owner, git | +| `parts-catalog-api` | Parts Catalog API | `team-development` | description, owner, git | +| `payments-service` | Payments Service | `team-development` | description, owner, git | +| `inventory-service` | Inventory Service | `team-operations` | description, owner, git | +| `ordering-service` | Ordering Service | `team-development` | description, owner, git | +| `shipping-service` | Shipping Service | `team-operations` | description, owner, git | + +### Scorecard (1) + +**Production Readiness** — Bronze/Silver/Gold applied to all services. + +| Level | Rules | +|---|---| +| Bronze | Has description, has owner team, has git configured | +| Silver | Has on-call configured, has runbook link, has `terraform-workspace` custom data | +| Gold | Description ≥ 50 characters, has groups set, owned by ≥ 2 teams | + +--- + +## Delta: What Changes + +The delta demonstrates three types of Terraform changes in one `terraform apply`: + +### 1. Service update — `phoenix` promoted Bronze → Silver +In `_templates/terraform-delta/services.tf`, the `phoenix` resource gains: +- `oncall` block (fictional PagerDuty policy) +- `links` block with a runbook URL +- `custom_data` block: `terraform-workspace = "phoenix-prod"` + +This satisfies all Silver rules. After apply, the scorecard score for `phoenix` updates. + +### 2. New service added — `notification-service` +A new `cortex_catalog_entity` resource is added for a `notification-service` (owner: `team-development`). Terraform creates it from scratch — no manual API call needed. + +### 3. Team member added — `team-development` +A new member (`Sarah Connor, sarah.connor@parts-unlimited.com`) is added to the Development team. Terraform updates only that resource in place. + +The README instructs the user to run `terraform plan` after copying the delta files to see the diff before applying. + +--- + +## Directory Structure + +``` +cortexapps_cli/solutions/terraform/ +├── README.md +├── setup.py +└── _templates/ + ├── terraform/ + │ ├── provider.tf + │ ├── variables.tf + │ ├── terraform.tfvars.example + │ ├── teams.tf + │ ├── domains.tf + │ ├── services.tf + │ └── scorecards.tf + └── terraform-delta/ + ├── services.tf # phoenix promoted to Silver + notification-service added + └── teams.tf # new member added to team-development +``` + +--- + +## setup.py Design + +### SETUP_DESCRIPTION + +``` +"Sets up the Parts Unlimited demo org in your Cortex instance using the Cortex Terraform provider. Requires Terraform >= 1.5 to be installed." +``` + +### `collect_prompts()` + +- `work_dir`: Working directory for Terraform files. Default: `~/parts-unlimited-terraform`. Env var: `TERRAFORM_WORK_DIR`. + +API key and base URL are available from the CLI session via `kwargs["cortex_api_key"]` and `kwargs["cortex_base_url"]` — no need to prompt. + +### `steps()` + +1. **Check Terraform** — runs `terraform version`, fails with a clear message if not found or version < 1.5. +2. **Create working directory** — `mkdir -p {work_dir}`. +3. **Copy Terraform files** — copies all files from `_templates/terraform/` into `{work_dir}`. +4. **Write terraform.tfvars** — writes `cortex_api_token` and `cortex_base_url` into `{work_dir}/terraform.tfvars` (not committed; `.gitignore` entry added). +5. **Write .gitignore** — adds `terraform.tfvars`, `.terraform/`, `*.tfstate*` to `{work_dir}/.gitignore`. +6. **terraform init** — runs `terraform init` in `{work_dir}`. +7. **terraform apply** — runs `terraform apply -auto-approve` in `{work_dir}`. + +### `post_steps()` + +Prints next-steps message: +- Where the files are (`{work_dir}`) +- How to run the delta: copy `_templates/terraform-delta/` files, run `terraform plan`, then `terraform apply` +- How customers wire this to CI (one-liner pointing to README) + +### Error handling + +- If Terraform is not installed: print install URL (`https://developer.hashicorp.com/terraform/install`), raise to abort. +- If `terraform init` or `terraform apply` fails: print stderr, raise. State files may be partially created — user can retry from `{work_dir}`. +- `mark_done()` is called after each step so re-runs skip completed steps. + +--- + +## Terraform Files + +### `provider.tf` + +Declares `cortexapps/cortex` provider `~> 0.6`, Terraform `>= 1.5`. Reads token and base URL from variables. + +### `variables.tf` + +Two variables: `cortex_api_token` (sensitive, no default) and `cortex_base_url` (default: `https://api.getcortexapp.com`). + +### `terraform.tfvars.example` + +Template with comments. Instructs user to copy to `terraform.tfvars` and never commit it. + +### `teams.tf` + +Four `cortex_catalog_entity` resources (type `team`) with member lists using realistic Parts Unlimited names. + +### `domains.tf` + +Two `cortex_catalog_entity` resources (type `domain`). Services are linked via `groups` — each service has a group matching the domain tag (e.g., `domain:ecommerce`). + +### `services.tf` + +Six `cortex_catalog_entity` resources (type `service`). Initial state intentionally at Bronze only: +- `description` — short, < 50 chars (so Gold rule fails too) +- `owner_teams` — one team per service +- `git` block — fictional GitHub repos under `github.com/parts-unlimited/` +- No `oncall`, no `links`, no `custom_data` (Silver rules not met) + +### `scorecards.tf` + +One `cortex_scorecard` resource: Production Readiness with Bronze/Silver/Gold rules as specified above. Filter: `types = ["service"]`. + +--- + +## Scorecard Expression Notes + +Cortex Expression Language (CEL-like) rules: + +| Level | Rule | Expression | +|---|---|---| +| Bronze | Has description | `entity.description().length > 0` | +| Bronze | Has owner team | `owners.teams.size() > 0` | +| Bronze | Has git configured | `git != null` | +| Silver | Has on-call | `oncall != null` | +| Silver | Has runbook link | `links.exists(l, l.type == "runbook")` | +| Silver | Has terraform-workspace | `customData.exists(d, d.key == "terraform-workspace")` | +| Gold | Description ≥ 50 chars | `entity.description().length >= 50` | +| Gold | Has groups | `groups.size() > 0` | +| Gold | Owned by ≥ 2 teams | `owners.teams.size() >= 2` | + +--- + +## README Structure + +``` +--- +name: Terraform +description: Manage your Cortex catalog as code using the Cortex Terraform provider. +--- + +# Terraform + +## What is the Cortex Terraform provider? +Brief explanation of HCL, providers, and how terraform apply → Cortex API. + +## Terraform vs. cortex.yaml — two flavors of GitOps +Centralized (Terraform) vs. decentralized (cortex.yaml per service repo). Neither is better; it depends on who owns the catalog. + +## What's Included +- Parts Unlimited demo org: 4 teams, 2 domains, 6 services, 1 scorecard +- Initial state: all services at Bronze on Production Readiness +- Delta: see services improve and a new service appear in one apply + +## Prerequisites +- Terraform >= 1.5 (https://developer.hashicorp.com/terraform/install) +- Cortex API key with write access + +## Install +`cortex solutions install -s terraform` +(Runs terraform apply. You'll be asked for a working directory.) + +## Explore what was created +- Link to Cortex catalog filtered by group `terraform-demo` +- Link to Production Readiness scorecard + +## Try the Delta +Step-by-step: copy delta files, terraform plan (shows diff), terraform apply, check scorecard. + +## File Walkthrough +One paragraph per .tf file explaining what it does and why. + +## Customizing for Your Org +How to adapt: rename entities, add services, extend scorecard rules. + +## Next Steps: CI/CD Integration +GitHub Actions snippet that runs terraform plan on PR and terraform apply on merge to main. +Explains that this is the standard customer workflow in practice. +``` + +--- + +## Out of Scope + +- No `catalog/` or `scorecards/` YAML files — Terraform is the sole source of truth +- No workflows +- No custom entity types or relationship types +- No GitHub Actions templates shipped with the solution (covered in README next steps only) +- Terraform state backend configuration (customers configure their own S3/GCS backend) +- Terraform destroy (not demonstrated; customers handle cleanup) From 569635c4dac9e9f066a25d13988877401d09d6ba Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:09:04 -0700 Subject: [PATCH 02/40] docs: update terraform solution spec with domain-split file structure (CX-43) Co-Authored-By: Claude Sonnet 4.6 --- .../2026-09-02-terraform-solution-design.md | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/docs/superpowers/specs/2026-09-02-terraform-solution-design.md b/docs/superpowers/specs/2026-09-02-terraform-solution-design.md index a47ab2eb..500c04b1 100644 --- a/docs/superpowers/specs/2026-09-02-terraform-solution-design.md +++ b/docs/superpowers/specs/2026-09-02-terraform-solution-design.md @@ -66,8 +66,16 @@ All services start at **Bronze** level on the Production Readiness scorecard. Th The delta demonstrates three types of Terraform changes in one `terraform apply`: +### File ownership model + +Terraform has no requirements on file names — it reads all `.tf` files in a directory and merges them. This means teams can own separate files, submit PRs touching only their file, and the catalog stays decentralized at the file level while remaining centralized in a single repo. + +Our demo makes this concrete: `ecommerce.tf` is owned by the e-commerce team, `supply-chain.tf` by the supply chain team. A PR to add a new e-commerce service touches only `ecommerce.tf`. Platform owns `teams.tf` and `scorecards.tf`. + +This pattern scales from a handful of services to 100K+ — at extreme scale, customers move to Terraform modules (subdirectories), but the ownership principle is the same. + ### 1. Service update — `phoenix` promoted Bronze → Silver -In `_templates/terraform-delta/services.tf`, the `phoenix` resource gains: +In `_templates/terraform-delta/ecommerce.tf`, the `phoenix` resource gains: - `oncall` block (fictional PagerDuty policy) - `links` block with a runbook URL - `custom_data` block: `terraform-workspace = "phoenix-prod"` @@ -75,12 +83,12 @@ In `_templates/terraform-delta/services.tf`, the `phoenix` resource gains: This satisfies all Silver rules. After apply, the scorecard score for `phoenix` updates. ### 2. New service added — `notification-service` -A new `cortex_catalog_entity` resource is added for a `notification-service` (owner: `team-development`). Terraform creates it from scratch — no manual API call needed. +A new `cortex_catalog_entity` resource is added to `ecommerce.tf` for a `notification-service` (owner: `team-development`). Terraform creates it from scratch — no manual API call needed. This is what adding a service looks like in a team's PR. ### 3. Team member added — `team-development` -A new member (`Sarah Connor, sarah.connor@parts-unlimited.com`) is added to the Development team. Terraform updates only that resource in place. +A new member (`Sarah Connor, sarah.connor@parts-unlimited.com`) is added in `_templates/terraform-delta/teams.tf`. Terraform updates only that resource in place. This is what onboarding someone looks like. -The README instructs the user to run `terraform plan` after copying the delta files to see the diff before applying. +The README instructs the user to run `terraform plan` after copying the delta files to see the diff before applying — specifically calling out which resources show as `~ update` vs `+ create`. --- @@ -92,16 +100,16 @@ cortexapps_cli/solutions/terraform/ ├── setup.py └── _templates/ ├── terraform/ - │ ├── provider.tf - │ ├── variables.tf + │ ├── provider.tf # provider config, required versions + │ ├── variables.tf # cortex_api_token, cortex_base_url │ ├── terraform.tfvars.example - │ ├── teams.tf - │ ├── domains.tf - │ ├── services.tf - │ └── scorecards.tf + │ ├── teams.tf # owned by: platform team + │ ├── ecommerce.tf # owned by: e-commerce team (domain + 3 services) + │ ├── supply-chain.tf # owned by: supply chain team (domain + 3 services) + │ └── scorecards.tf # owned by: platform team └── terraform-delta/ - ├── services.tf # phoenix promoted to Silver + notification-service added - └── teams.tf # new member added to team-development + ├── ecommerce.tf # phoenix → Silver, notification-service added + └── teams.tf # new member on team-development ``` --- @@ -159,25 +167,27 @@ Two variables: `cortex_api_token` (sensitive, no default) and `cortex_base_url` Template with comments. Instructs user to copy to `terraform.tfvars` and never commit it. -### `teams.tf` +### `teams.tf` (platform-owned) + +Four `cortex_catalog_entity` resources (type `team`) with member lists using realistic Parts Unlimited names (Bill, Brent, John, etc.). -Four `cortex_catalog_entity` resources (type `team`) with member lists using realistic Parts Unlimited names. +### `ecommerce.tf` (e-commerce team-owned) -### `domains.tf` +One `cortex_catalog_entity` resource (type `domain`) for `domain-ecommerce`, followed by three service resources: `phoenix`, `parts-catalog-api`, `payments-service`. Domain and its services co-located — the e-commerce team owns everything in this file. -Two `cortex_catalog_entity` resources (type `domain`). Services are linked via `groups` — each service has a group matching the domain tag (e.g., `domain:ecommerce`). +Services initial state (intentionally Bronze only): +- `description` — short, < 50 chars +- `owner_teams` — `team-development` +- `git` block — fictional repos under `github.com/parts-unlimited/` +- No `oncall`, no `links`, no `custom_data` -### `services.tf` +### `supply-chain.tf` (supply chain team-owned) -Six `cortex_catalog_entity` resources (type `service`). Initial state intentionally at Bronze only: -- `description` — short, < 50 chars (so Gold rule fails too) -- `owner_teams` — one team per service -- `git` block — fictional GitHub repos under `github.com/parts-unlimited/` -- No `oncall`, no `links`, no `custom_data` (Silver rules not met) +Same pattern: one `domain` resource (`domain-supply-chain`) and three service resources: `inventory-service`, `ordering-service`, `shipping-service`. Owner: `team-operations`. -### `scorecards.tf` +### `scorecards.tf` (platform-owned) -One `cortex_scorecard` resource: Production Readiness with Bronze/Silver/Gold rules as specified above. Filter: `types = ["service"]`. +One `cortex_scorecard` resource: Production Readiness with Bronze/Silver/Gold rules. Filter: `types = ["service"]`. --- From 2c67e0e2d63cbea7746d2916c897dbf581f72b17 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:17:59 -0700 Subject: [PATCH 03/40] docs: add terraform solution implementation plan (CX-43) Co-Authored-By: Claude Sonnet 4.6 --- .../plans/2026-09-02-terraform-solution.md | 1407 +++++++++++++++++ 1 file changed, 1407 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-02-terraform-solution.md diff --git a/docs/superpowers/plans/2026-09-02-terraform-solution.md b/docs/superpowers/plans/2026-09-02-terraform-solution.md new file mode 100644 index 00000000..64aa6324 --- /dev/null +++ b/docs/superpowers/plans/2026-09-02-terraform-solution.md @@ -0,0 +1,1407 @@ +# Terraform Solution Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a `terraform` Cortex solution bundle that lets users run `cortex solutions post-install -s terraform` to apply a real `terraform apply`, creating a Parts Unlimited demo org in their Cortex instance. + +**Architecture:** A setup.py script checks for the Terraform CLI, copies `.tf` template files to a working directory, writes credentials to `terraform.tfvars`, and runs `terraform init` + `terraform apply`. No YAML catalog files — Terraform is the sole source of truth. A `_templates/terraform-delta/` directory contains modified `.tf` files users can drop in to trigger a second apply that promotes a service from Bronze to Silver on the Production Readiness scorecard. + +**Tech Stack:** Python 3.11+, HCL (Terraform), `cortexapps/cortex` Terraform provider `~> 0.6`, `subprocess` for shell invocation, `SolutionSetup` base class from `cortexapps_cli.solutions._lib.setup_base`. + +**Spec:** `docs/superpowers/specs/2026-09-02-terraform-solution-design.md` + +## Global Constraints + +- Terraform provider: `cortexapps/cortex ~> 0.6`, Terraform CLI `>= 1.5` +- Entity tags: kebab-case (`team-development`, `phoenix`, `domain-ecommerce`) +- All entities tagged with group `terraform-demo` for easy filtering/cleanup +- No `catalog/` or `scorecards/` YAML files — Terraform only +- No `on_call` rules in scorecard — demo must work without external integrations (PagerDuty etc.) +- setup.py entry point: `main(**kwargs)` — `kwargs["cortex_api_key"]` and `kwargs["cortex_base_url"]` come from CLI session +- setup.py run via: `cortex solutions post-install -s terraform` +- State file: `~/.cortex/solutions/terraform.json` (managed by SolutionSetup base class) +- `mark_done()` / `already_done()` used for every step so re-runs skip completed steps +- Parts Unlimited theme throughout — characters from *The Phoenix Project* (Bill, Brent, John) +- Services intentionally start at Bronze only (no links, no metadata) so delta is meaningful + +--- + +## File Map + +| File | Action | Purpose | +|---|---|---| +| `cortexapps_cli/solutions/terraform/README.md` | Create | Solution README with frontmatter, full docs, delta walkthrough | +| `cortexapps_cli/solutions/terraform/setup.py` | Create | Post-install script: terraform check → copy → tfvars → init → apply | +| `cortexapps_cli/solutions/terraform/_templates/terraform/provider.tf` | Create | Terraform provider config | +| `cortexapps_cli/solutions/terraform/_templates/terraform/variables.tf` | Create | Input variables for token and base URL | +| `cortexapps_cli/solutions/terraform/_templates/terraform/terraform.tfvars.example` | Create | Template tfvars with comments | +| `cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf` | Create | 4 team entities (Development, Operations, Security, QA) | +| `cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf` | Create | domain-ecommerce + 3 Bronze services (phoenix, parts-catalog-api, payments-service) | +| `cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf` | Create | domain-supply-chain + 3 Bronze services (inventory, ordering, shipping) | +| `cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf` | Create | Production Readiness scorecard (Bronze/Silver/Gold) | +| `cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf` | Create | ecommerce.tf with phoenix → Silver + notification-service added | +| `cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf` | Create | teams.tf with Sarah Connor added to team-development | + +--- + +## Task 1: Scaffold solution directory and README + +**Files:** +- Create: `cortexapps_cli/solutions/terraform/README.md` + +**Interfaces:** +- Produces: `cortexapps_cli/solutions/terraform/` directory; `cortex solutions list` will show "Terraform" entry; `cortex solutions info -s terraform` will show the README body + +- [ ] **Step 1: Create the solution directory** + +```bash +mkdir -p cortexapps_cli/solutions/terraform/_templates/terraform +mkdir -p cortexapps_cli/solutions/terraform/_templates/terraform-delta +``` + +- [ ] **Step 2: Write README.md** + +Create `cortexapps_cli/solutions/terraform/README.md` with this exact content: + +````markdown +--- +name: Terraform +description: Manage your Cortex catalog as code using the Cortex Terraform provider. +--- + +# Terraform + +The [Cortex Terraform provider](https://github.com/cortexapps/terraform-provider-cortex) lets you define your entire service catalog — teams, services, domains, scorecards — as HCL code in `.tf` files. Changes go through PR review and apply automatically on merge, giving you a fully auditable, GitOps-driven catalog. + +## What is HCL? + +HCL (HashiCorp Configuration Language) is the declarative language used in `.tf` files. It reads like structured config rather than code: + +```hcl +resource "cortex_catalog_entity" "phoenix" { + tag = "phoenix" + name = "The Phoenix Project" + description = "Main e-commerce monolith for Parts Unlimited." + + owners = [{ name = "team-development", type = "group", provider = "CORTEX" }] + + git = { + github = { repository = "parts-unlimited/phoenix" } + } +} +``` + +Terraform reads all `.tf` files in a directory, compares them to the current live state, and applies only what changed. + +## Terraform vs. cortex.yaml — two flavors of GitOps + +Both approaches keep your catalog in git and apply changes on merge. The difference is where the files live: + +| | Terraform | cortex.yaml | +|---|---|---| +| **Files live in** | One central infra repo | Each service's own repo | +| **Managed by** | Platform / infra team | Individual service teams | +| **Also manages** | AWS, GCP, everything else | Just Cortex catalog | +| **Apply mechanism** | CI runs `terraform apply` | Cortex git integration polls files | + +Neither is better. Platform teams who already manage cloud infrastructure with Terraform often prefer the centralized approach. Dev teams who want catalog config alongside their code prefer cortex.yaml. + +## File ownership at scale + +Terraform has no requirements on file names — it reads all `.tf` files in a directory and merges them. This means you can split by team or domain ownership: + +``` +infra-catalog/ +├── provider.tf ← platform team +├── teams.tf ← platform team +├── scorecards.tf ← platform team +├── ecommerce.tf ← e-commerce team (domain + services) +├── supply-chain.tf ← supply chain team (domain + services) +└── payments.tf ← payments team +``` + +Each team submits PRs touching only their file. At very large scale (100K+ services), teams use Terraform modules (subdirectories) to organize further — but the ownership model is the same. + +## What's Included + +This solution installs a demo org for **Parts Unlimited** (from *The Phoenix Project*): + +- **4 teams**: Development (Bill's team), IT Operations (Brent's domain), Information Security (John's team), QA +- **2 domains**: E-Commerce, Supply Chain +- **6 services**: The Phoenix Project, Parts Catalog API, Payments Service, Inventory Service, Ordering Service, Shipping Service +- **1 scorecard**: Production Readiness (Bronze/Silver/Gold) + +All services start at **Bronze** — intentionally incomplete so you can see the delta in action. + +## Prerequisites + +- Terraform >= 1.5 — install at https://developer.hashicorp.com/terraform/install +- Cortex API key with write access + +## Install + +```bash +cortex solutions post-install -s terraform +``` + +You'll be asked where to create the working directory (default: `~/parts-unlimited-terraform`). The script will: +1. Verify Terraform is installed +2. Copy the `.tf` files to your working directory +3. Write your credentials to `terraform.tfvars` +4. Run `terraform init` to download the Cortex provider +5. Run `terraform apply` to create all entities in Cortex + +## Explore what was created + +After install, browse your Cortex catalog filtered by the `terraform-demo` group to see all created entities. Open the **Production Readiness** scorecard to see all 6 services at Bronze. + +## Try the Delta + +The delta shows what a real team PR looks like — modify a file, plan, apply, watch the scorecard update. + +**Step 1: Copy the delta files into your working directory** + +```bash +cp /terraform-delta/ecommerce.tf ~/parts-unlimited-terraform/ecommerce.tf +cp /terraform-delta/teams.tf ~/parts-unlimited-terraform/teams.tf +``` + +> The delta files are in `_templates/terraform-delta/` inside the installed solutions package. Run `cortex solutions info -s terraform` to find the exact path. + +**Step 2: See what will change** + +```bash +cd ~/parts-unlimited-terraform +terraform plan +``` + +You'll see: +- `~ cortex_catalog_entity.phoenix` — **update** (adds links, metadata) +- `+ cortex_catalog_entity.notification_service` — **create** (new service) +- `~ cortex_catalog_entity.team_development` — **update** (new team member) + +**Step 3: Apply** + +```bash +terraform apply +``` + +**Step 4: Check the scorecard** + +Open Production Readiness in Cortex. The Phoenix Project should now show **Silver**. + +## File Walkthrough + +**`provider.tf`** — Declares the `cortexapps/cortex` provider version and reads credentials from variables. This is the only file that changes if you upgrade the provider version. + +**`variables.tf`** — Defines `cortex_api_token` (sensitive) and `cortex_base_url`. Values come from `terraform.tfvars` (never committed) or environment variables (`CORTEX_API_TOKEN`, `CORTEX_API_URL`). + +**`teams.tf`** — Owned by the platform team. Defines all teams and their members. Changes here require a platform PR. + +**`ecommerce.tf`** — Owned by the e-commerce team. Defines the E-Commerce domain and its three services. Changes here — new services, updated descriptions, added links — are the e-commerce team's PR to make. + +**`supply-chain.tf`** — Same pattern, owned by the supply chain team. + +**`scorecards.tf`** — Owned by the platform team. Defines the Production Readiness scorecard and its Bronze/Silver/Gold rules. + +## Customizing for Your Org + +1. Rename entity tags and display names throughout +2. Replace `parts-unlimited/*` GitHub repos with your actual repos +3. Add more services: copy any service block from `ecommerce.tf` and adjust the tag, name, and owner +4. Extend scorecard rules: add rules to `scorecards.tf` with the expression language shown in the [Cortex docs](https://docs.cortex.io/docs/reference/scorecard-rules) +5. Split into more files as your team grows — Terraform reads them all + +## Next Steps: CI/CD Integration + +In practice, customers commit their `.tf` files to a repo and let CI handle applies. Here's a minimal GitHub Actions workflow: + +```yaml +# .github/workflows/cortex-catalog.yml +name: Cortex Catalog + +on: + pull_request: + paths: ['catalog/**'] + push: + branches: [main] + paths: ['catalog/**'] + +jobs: + plan: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: "~> 1.5" + - run: terraform init + working-directory: catalog + env: + TF_VAR_cortex_api_token: ${{ secrets.CORTEX_API_TOKEN }} + - run: terraform plan + working-directory: catalog + env: + TF_VAR_cortex_api_token: ${{ secrets.CORTEX_API_TOKEN }} + + apply: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: "~> 1.5" + - run: terraform init + working-directory: catalog + env: + TF_VAR_cortex_api_token: ${{ secrets.CORTEX_API_TOKEN }} + - run: terraform apply -auto-approve + working-directory: catalog + env: + TF_VAR_cortex_api_token: ${{ secrets.CORTEX_API_TOKEN }} +``` + +Store `CORTEX_API_TOKEN` as a GitHub Actions secret. Now every PR shows a plan diff as a CI check, and every merge to main applies automatically. +```` + +- [ ] **Step 3: Verify the README appears in `cortex solutions list`** + +```bash +poetry run cortex solutions list +``` + +Expected: a row showing "Terraform" with description "Manage your Cortex catalog as code using the Cortex Terraform provider." + +- [ ] **Step 4: Commit** + +```bash +git add cortexapps_cli/solutions/terraform/ +git commit -m "feat: add terraform solution scaffold and README (CX-43)" +``` + +--- + +## Task 2: Terraform provider config files + +**Files:** +- Create: `cortexapps_cli/solutions/terraform/_templates/terraform/provider.tf` +- Create: `cortexapps_cli/solutions/terraform/_templates/terraform/variables.tf` +- Create: `cortexapps_cli/solutions/terraform/_templates/terraform/terraform.tfvars.example` + +**Interfaces:** +- Produces: three files consumed by `terraform init` and `terraform apply` in all subsequent tasks; `var.cortex_api_token` and `var.cortex_base_url` are referenced by all resource files + +- [ ] **Step 1: Write provider.tf** + +```hcl +terraform { + required_providers { + cortex = { + source = "cortexapps/cortex" + version = "~> 0.6" + } + } + required_version = ">= 1.5" +} + +provider "cortex" { + token = var.cortex_api_token + base_api_url = var.cortex_base_url +} +``` + +- [ ] **Step 2: Write variables.tf** + +```hcl +variable "cortex_api_token" { + description = "Cortex API token. Can also be set via the CORTEX_API_TOKEN environment variable." + type = string + sensitive = true +} + +variable "cortex_base_url" { + description = "Cortex API base URL." + type = string + default = "https://api.getcortexapp.com" +} +``` + +- [ ] **Step 3: Write terraform.tfvars.example** + +```hcl +# Copy this file to terraform.tfvars and fill in your values. +# IMPORTANT: Never commit terraform.tfvars to source control — it contains your API token. +# Add terraform.tfvars to your .gitignore. + +# Your Cortex API token. +# Alternatively, set the CORTEX_API_TOKEN environment variable and omit this line. +# cortex_api_token = "your-api-token-here" + +# Cortex API base URL. Only change if you are on a self-hosted instance. +cortex_base_url = "https://api.getcortexapp.com" +``` + +- [ ] **Step 4: Verify files are present** + +```bash +ls cortexapps_cli/solutions/terraform/_templates/terraform/ +``` + +Expected output includes: `provider.tf variables.tf terraform.tfvars.example` + +- [ ] **Step 5: Commit** + +```bash +git add cortexapps_cli/solutions/terraform/_templates/terraform/ +git commit -m "feat: add terraform provider config files (CX-43)" +``` + +--- + +## Task 3: teams.tf + +**Files:** +- Create: `cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf` + +**Interfaces:** +- Produces: team tags `team-development`, `team-operations`, `team-security`, `team-qa` — referenced by `owners` blocks in ecommerce.tf and supply-chain.tf + +- [ ] **Step 1: Write teams.tf** + +```hcl +# teams.tf — Platform-owned +# Changes to teams (membership, new hires, reorgs) are made here via platform PR. + +resource "cortex_catalog_entity" "team_development" { + tag = "team-development" + name = "Development" + description = "Application development team responsible for Parts Unlimited's e-commerce platform and core services." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "Bill Palmer" + email = "bill.palmer@parts-unlimited.com" + role = "Team Lead" + description = "IT Manager leading the Phoenix Project" + }, + { + name = "Maxine Chambers" + email = "maxine.chambers@parts-unlimited.com" + role = "Senior Engineer" + description = "Staff engineer on the Phoenix Project" + }, + { + name = "Dev Magee" + email = "dev.magee@parts-unlimited.com" + role = "Engineer" + description = "Developer on the Phoenix Project" + } + ] + } +} + +resource "cortex_catalog_entity" "team_operations" { + tag = "team-operations" + name = "IT Operations" + description = "Infrastructure, reliability, and operations for Parts Unlimited's production systems." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "Brent Geller" + email = "brent.geller@parts-unlimited.com" + role = "Principal Engineer" + description = "Indispensable operations expert and bottleneck" + }, + { + name = "Wes Davis" + email = "wes.davis@parts-unlimited.com" + role = "Operations Manager" + description = "Manages day-to-day operations work" + } + ] + } +} + +resource "cortex_catalog_entity" "team_security" { + tag = "team-security" + name = "Information Security" + description = "Security, compliance, and risk management for Parts Unlimited." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "John Pesche" + email = "john.pesche@parts-unlimited.com" + role = "CISO" + description = "Chief Information Security Officer" + } + ] + } +} + +resource "cortex_catalog_entity" "team_qa" { + tag = "team-qa" + name = "Quality Assurance" + description = "Testing, QA, and release verification for Parts Unlimited services." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "Patty McKee" + email = "patty.mckee@parts-unlimited.com" + role = "QA Manager" + description = "Manages QA processes and testing" + } + ] + } +} +``` + +> **Note on `team` block syntax:** The exact nested HCL for team members may differ from what's shown. Verify against the provider docs at https://registry.terraform.io/providers/cortexapps/cortex/latest/docs/resources/catalog_entity — look for `team` and `members` attributes. If `team { members [...] }` doesn't work, the provider may use a flat `members` block at the resource level. Run `terraform validate` after writing and fix any schema errors. + +- [ ] **Step 2: Commit** + +```bash +git add cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf +git commit -m "feat: add teams.tf for terraform solution (CX-43)" +``` + +--- + +## Task 4: ecommerce.tf (initial Bronze state) + +**Files:** +- Create: `cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf` + +**Interfaces:** +- Produces: entity tags `domain-ecommerce`, `phoenix`, `parts-catalog-api`, `payments-service` +- Consumes: team tag `team-development` (from Task 3) + +- [ ] **Step 1: Write ecommerce.tf** + +```hcl +# ecommerce.tf — E-Commerce team-owned +# This file defines the E-Commerce domain and all services within it. +# The e-commerce team submits PRs to this file to add/update services. +# +# NOTE: Services are intentionally at Bronze level only (no links, no metadata). +# See _templates/terraform-delta/ecommerce.tf for the Silver-state version. + +resource "cortex_catalog_entity" "domain_ecommerce" { + tag = "domain-ecommerce" + name = "E-Commerce" + description = "Customer-facing e-commerce platform including product catalog, checkout, and payments." + type = "domain" + + groups = ["terraform-demo"] +} + +resource "cortex_catalog_entity" "phoenix" { + tag = "phoenix" + name = "The Phoenix Project" + description = "Main e-commerce monolith handling browsing and checkout." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/phoenix" + base_path = "/" + } + } +} + +resource "cortex_catalog_entity" "parts_catalog_api" { + tag = "parts-catalog-api" + name = "Parts Catalog API" + description = "REST API for browsing the parts catalog." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/parts-catalog-api" + base_path = "/" + } + } +} + +resource "cortex_catalog_entity" "payments_service" { + tag = "payments-service" + name = "Payments Service" + description = "Payment processing and refund handling." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/payments-service" + base_path = "/" + } + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf +git commit -m "feat: add ecommerce.tf for terraform solution (CX-43)" +``` + +--- + +## Task 5: supply-chain.tf (initial Bronze state) + +**Files:** +- Create: `cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf` + +**Interfaces:** +- Produces: entity tags `domain-supply-chain`, `inventory-service`, `ordering-service`, `shipping-service` +- Consumes: team tag `team-operations` (from Task 3) + +- [ ] **Step 1: Write supply-chain.tf** + +```hcl +# supply-chain.tf — Supply Chain team-owned +# This file defines the Supply Chain domain and all services within it. +# The supply chain team submits PRs to this file to add/update services. + +resource "cortex_catalog_entity" "domain_supply_chain" { + tag = "domain-supply-chain" + name = "Supply Chain" + description = "Inventory, ordering, and shipping services supporting Parts Unlimited's fulfillment operations." + type = "domain" + + groups = ["terraform-demo"] +} + +resource "cortex_catalog_entity" "inventory_service" { + tag = "inventory-service" + name = "Inventory Service" + description = "Real-time inventory tracking across all Parts Unlimited warehouses." + + owners = [ + { + name = "team-operations" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:supply-chain"] + + git = { + github = { + repository = "parts-unlimited/inventory-service" + base_path = "/" + } + } +} + +resource "cortex_catalog_entity" "ordering_service" { + tag = "ordering-service" + name = "Ordering Service" + description = "Order placement, validation, and fulfillment coordination." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:supply-chain"] + + git = { + github = { + repository = "parts-unlimited/ordering-service" + base_path = "/" + } + } +} + +resource "cortex_catalog_entity" "shipping_service" { + tag = "shipping-service" + name = "Shipping Service" + description = "Shipping and logistics tracking for Parts Unlimited orders." + + owners = [ + { + name = "team-operations" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:supply-chain"] + + git = { + github = { + repository = "parts-unlimited/shipping-service" + base_path = "/" + } + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf +git commit -m "feat: add supply-chain.tf for terraform solution (CX-43)" +``` + +--- + +## Task 6: scorecards.tf + +**Files:** +- Create: `cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf` + +**Interfaces:** +- Produces: scorecard tag `production-readiness`; Bronze rules pass for all initial services; Silver rules intentionally fail until delta is applied + +- [ ] **Step 1: Write scorecards.tf** + +The scorecard uses only Terraform-native rules — no external integrations (no PagerDuty, no OpsGenie) required. Services start at Bronze and the delta moves `phoenix` to Silver. + +```hcl +# scorecards.tf — Platform-owned +# Defines the Production Readiness scorecard. +# Bronze: automatically achieved by all properly-defined services. +# Silver: requires adding links and metadata — see the delta. +# Gold: requires shared ownership and a rich description — aspirational. + +resource "cortex_scorecard" "production_readiness" { + tag = "production-readiness" + name = "Production Readiness" + description = "Measures how production-ready a Parts Unlimited service is. Bronze is table stakes; Gold is the aspirational standard." + draft = false + + ladder = { + levels = [ + { + name = "Gold" + rank = 3 + color = "#D7AC58" + }, + { + name = "Silver" + rank = 2 + color = "#C0C0C0" + }, + { + name = "Bronze" + rank = 1 + color = "#CD7F32" + } + ] + } + + rules = [ + # ── Bronze ──────────────────────────────────────────────────────────────── + { + title = "Has description" + description = "Service must have a non-empty description." + expression = "entity.description().length > 0" + weight = 1 + level = "Bronze" + }, + { + title = "Has owner team" + description = "Service must be owned by at least one team." + expression = "owners.teams.size() > 0" + weight = 1 + level = "Bronze" + }, + { + title = "Has git configured" + description = "Service must have a git repository linked." + expression = "git != null" + weight = 1 + level = "Bronze" + }, + + # ── Silver ──────────────────────────────────────────────────────────────── + { + title = "Has at least one link" + description = "Service must have at least one link (runbook, docs, dashboard, etc.)." + expression = "links.size() > 0" + weight = 1 + level = "Silver" + }, + { + title = "Has terraform-workspace metadata" + description = "Service must declare its Terraform workspace via the terraform-workspace metadata key." + expression = "customData.exists(d, d.key == \"terraform-workspace\")" + weight = 1 + level = "Silver" + }, + { + title = "Meaningful description" + description = "Service description should be at least 30 characters." + expression = "entity.description().length >= 30" + weight = 1 + level = "Silver" + }, + + # ── Gold ────────────────────────────────────────────────────────────────── + { + title = "Rich description" + description = "Service description should be at least 50 characters." + expression = "entity.description().length >= 50" + weight = 1 + level = "Gold" + }, + { + title = "Shared ownership" + description = "Critical services should be owned by at least two teams to avoid single points of knowledge." + expression = "owners.teams.size() >= 2" + weight = 1 + level = "Gold" + }, + { + title = "Has runbook" + description = "Service must have a runbook link for on-call responders." + expression = "links.exists(l, l.type == \"runbook\")" + weight = 1 + level = "Gold" + } + ] + + filter = { + types = { + include = ["service"] + } + } + + evaluation = { + window = 24 + } +} +``` + +> **Note on Cortex expression language:** `owners.teams.size()` and `customData.exists(...)` are Cortex Expression Language (CEL-like). Verify these expressions work against your Cortex instance by checking the scorecard after `terraform apply`. If a Bronze rule shows as failing for a service that clearly has an owner, the expression path may need adjusting — check Cortex scorecard rule documentation for the correct field names. + +- [ ] **Step 2: Commit** + +```bash +git add cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +git commit -m "feat: add scorecards.tf for terraform solution (CX-43)" +``` + +--- + +## Task 7: terraform-delta files + +**Files:** +- Create: `cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf` +- Create: `cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf` + +**Interfaces:** +- Produces: drop-in replacement files for `_templates/terraform/ecommerce.tf` and `teams.tf`; when copied into a working directory and applied, `phoenix` gains Silver, `notification-service` is created, Sarah Connor joins `team-development` +- Consumes: same entity tags and owner references as Tasks 3 and 4 — these files must be self-contained complete replacements (all original resources present + changes) + +- [ ] **Step 1: Write terraform-delta/ecommerce.tf** + +This is a complete replacement for `ecommerce.tf`. It includes all original resources unchanged, plus: `phoenix` gets links + metadata (Bronze → Silver), `notification-service` is added. + +```hcl +# terraform-delta/ecommerce.tf +# ───────────────────────────────────────────────────────────────────────────── +# DELTA VERSION — copy over ecommerce.tf and run `terraform plan` to see diff. +# +# Changes from baseline: +# phoenix → promoted to Silver (links + metadata added, description expanded) +# notification-service → NEW service (+ create in plan output) +# +# Unchanged: domain-ecommerce, parts-catalog-api, payments-service +# ───────────────────────────────────────────────────────────────────────────── + +resource "cortex_catalog_entity" "domain_ecommerce" { + tag = "domain-ecommerce" + name = "E-Commerce" + description = "Customer-facing e-commerce platform including product catalog, checkout, and payments." + type = "domain" + + groups = ["terraform-demo"] +} + +# CHANGED: description expanded (≥ 30 chars for Silver rule 3), +# links added (Silver rule 1), metadata added (Silver rule 2) +resource "cortex_catalog_entity" "phoenix" { + tag = "phoenix" + name = "The Phoenix Project" + description = "Main e-commerce monolith for Parts Unlimited, handling product browsing, cart, and checkout flows." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/phoenix" + base_path = "/" + } + } + + links = [ + { + name = "Runbook" + type = "runbook" + url = "https://wiki.parts-unlimited.com/runbooks/phoenix" + }, + { + name = "Architecture Docs" + type = "documentation" + url = "https://wiki.parts-unlimited.com/architecture/phoenix" + } + ] + + metadata = jsonencode({ + "terraform-workspace" = "phoenix-prod" + }) +} + +resource "cortex_catalog_entity" "parts_catalog_api" { + tag = "parts-catalog-api" + name = "Parts Catalog API" + description = "REST API for browsing the parts catalog." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/parts-catalog-api" + base_path = "/" + } + } +} + +resource "cortex_catalog_entity" "payments_service" { + tag = "payments-service" + name = "Payments Service" + description = "Payment processing and refund handling." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/payments-service" + base_path = "/" + } + } +} + +# NEW SERVICE — will show as `+ create` in terraform plan +resource "cortex_catalog_entity" "notification_service" { + tag = "notification-service" + name = "Notification Service" + description = "Handles email, SMS, and push notifications for Parts Unlimited customer events." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/notification-service" + base_path = "/" + } + } +} +``` + +- [ ] **Step 2: Write terraform-delta/teams.tf** + +Complete replacement for `teams.tf`. All original teams unchanged; Sarah Connor added to `team-development`. + +```hcl +# terraform-delta/teams.tf +# ───────────────────────────────────────────────────────────────────────────── +# DELTA VERSION — copy over teams.tf and run `terraform plan` to see diff. +# +# Changes from baseline: +# team-development → Sarah Connor added (~ update in plan output) +# +# Unchanged: team-operations, team-security, team-qa +# ───────────────────────────────────────────────────────────────────────────── + +# CHANGED: Sarah Connor added +resource "cortex_catalog_entity" "team_development" { + tag = "team-development" + name = "Development" + description = "Application development team responsible for Parts Unlimited's e-commerce platform and core services." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "Bill Palmer" + email = "bill.palmer@parts-unlimited.com" + role = "Team Lead" + description = "IT Manager leading the Phoenix Project" + }, + { + name = "Maxine Chambers" + email = "maxine.chambers@parts-unlimited.com" + role = "Senior Engineer" + description = "Staff engineer on the Phoenix Project" + }, + { + name = "Dev Magee" + email = "dev.magee@parts-unlimited.com" + role = "Engineer" + description = "Developer on the Phoenix Project" + }, + { + name = "Sarah Connor" + email = "sarah.connor@parts-unlimited.com" + role = "Engineer" + description = "New hire joining the Phoenix Project team" + } + ] + } +} + +resource "cortex_catalog_entity" "team_operations" { + tag = "team-operations" + name = "IT Operations" + description = "Infrastructure, reliability, and operations for Parts Unlimited's production systems." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "Brent Geller" + email = "brent.geller@parts-unlimited.com" + role = "Principal Engineer" + description = "Indispensable operations expert and bottleneck" + }, + { + name = "Wes Davis" + email = "wes.davis@parts-unlimited.com" + role = "Operations Manager" + description = "Manages day-to-day operations work" + } + ] + } +} + +resource "cortex_catalog_entity" "team_security" { + tag = "team-security" + name = "Information Security" + description = "Security, compliance, and risk management for Parts Unlimited." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "John Pesche" + email = "john.pesche@parts-unlimited.com" + role = "CISO" + description = "Chief Information Security Officer" + } + ] + } +} + +resource "cortex_catalog_entity" "team_qa" { + tag = "team-qa" + name = "Quality Assurance" + description = "Testing, QA, and release verification for Parts Unlimited services." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "Patty McKee" + email = "patty.mckee@parts-unlimited.com" + role = "QA Manager" + description = "Manages QA processes and testing" + } + ] + } +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add cortexapps_cli/solutions/terraform/_templates/terraform-delta/ +git commit -m "feat: add terraform-delta files for terraform solution (CX-43)" +``` + +--- + +## Task 8: setup.py + +**Files:** +- Create: `cortexapps_cli/solutions/terraform/setup.py` + +**Interfaces:** +- Consumes: `kwargs["cortex_api_key"]`, `kwargs["cortex_base_url"]` from CLI session; `_templates/terraform/` directory (Tasks 2–6); `SolutionSetup` from `cortexapps_cli.solutions._lib.setup_base` +- Produces: entry point `main(**kwargs)` invoked by `cortex solutions post-install -s terraform`; working directory at user-specified path with all `.tf` files and `terraform.tfvars`; entities created in Cortex via `terraform apply` + +- [ ] **Step 1: Write setup.py** + +```python +""" +Post-install setup for the terraform solution. +Verifies Terraform is installed, copies template files to a working directory, +writes credentials, and runs terraform init + apply. + +Run via: cortex solutions post-install -s terraform +""" + +SETUP_DESCRIPTION = ( + "Sets up the Parts Unlimited demo org in your Cortex instance using the " + "Cortex Terraform provider. Requires Terraform >= 1.5 — install at " + "https://developer.hashicorp.com/terraform/install" +) + +import shutil +import subprocess +import sys +from pathlib import Path + +try: + from cortexapps_cli.solutions._lib.setup_base import SolutionSetup +except ImportError: + sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + from _lib.setup_base import SolutionSetup + +_TEMPLATES_DIR = Path(__file__).parent / "_templates" / "terraform" +_DELTA_DIR = Path(__file__).parent / "_templates" / "terraform-delta" + +_GITIGNORE_ENTRIES = [ + "terraform.tfvars", + ".terraform/", + "*.tfstate", + "*.tfstate.backup", + ".terraform.lock.hcl", +] + + +class TerraformSetup(SolutionSetup): + solution_tag = "terraform" + + def collect_prompts(self) -> None: + self.prompt( + "work_dir", + "Working directory for Terraform files", + default=str(Path.home() / "parts-unlimited-terraform"), + env_var="TERRAFORM_WORK_DIR", + ) + + def steps(self) -> list[tuple[str, callable]]: + return [ + ("Check Terraform CLI", self._check_terraform), + ("Create working directory", self._create_work_dir), + ("Copy Terraform files", self._copy_files), + ("Write terraform.tfvars", self._write_tfvars), + ("Write .gitignore", self._write_gitignore), + ("terraform init", self._terraform_init), + ("terraform apply", self._terraform_apply), + ] + + def post_steps(self) -> None: + work_dir = self._answers["work_dir"] + delta_dir = _DELTA_DIR + + print("\n✓ Parts Unlimited demo org created in Cortex via Terraform!\n") + print(f" Terraform files are at: {work_dir}\n") + print("─" * 60) + print("NEXT: Try the delta to see Terraform's incremental update\n") + print(" 1. Copy the delta files into your working directory:") + print(f" cp {delta_dir}/ecommerce.tf {work_dir}/ecommerce.tf") + print(f" cp {delta_dir}/teams.tf {work_dir}/teams.tf\n") + print(" 2. Preview the changes:") + print(f" cd {work_dir} && terraform plan\n") + print(" Look for:") + print(" ~ cortex_catalog_entity.phoenix (update: links + metadata added)") + print(" + cortex_catalog_entity.notification_service (create: new service)") + print(" ~ cortex_catalog_entity.team_development (update: new member)\n") + print(" 3. Apply:") + print(f" terraform apply\n") + print(" 4. Check the Production Readiness scorecard in Cortex.") + print(" The Phoenix Project should now show Silver.\n") + print("─" * 60) + print("To use Terraform for your real catalog, see the CI/CD integration") + print("section in: cortex solutions info -s terraform") + + # ── Private step implementations ────────────────────────────────────────── + + def _check_terraform(self) -> None: + if self.already_done("check_terraform"): + return + result = shutil.which("terraform") + if result is None: + print( + "\nERROR: terraform CLI not found in PATH.\n" + "Install Terraform >= 1.5 from: https://developer.hashicorp.com/terraform/install", + file=sys.stderr, + ) + raise RuntimeError("terraform not found") + # Check version >= 1.5 + try: + out = subprocess.check_output( + ["terraform", "version", "-json"], text=True + ) + import json + version_str = json.loads(out).get("terraform_version", "0.0.0") + major, minor, *_ = (int(x) for x in version_str.split(".")) + if (major, minor) < (1, 5): + raise RuntimeError( + f"Terraform {version_str} is too old. Version >= 1.5 required.\n" + "Upgrade at: https://developer.hashicorp.com/terraform/install" + ) + except (subprocess.CalledProcessError, KeyError, ValueError): + # If version check fails, proceed — let terraform itself error if needed + pass + self.mark_done("check_terraform") + + def _create_work_dir(self) -> None: + if self.already_done("create_work_dir"): + return + work_dir = Path(self._answers["work_dir"]).expanduser() + work_dir.mkdir(parents=True, exist_ok=True) + self.mark_done("create_work_dir") + + def _copy_files(self) -> None: + if self.already_done("copy_files"): + return + work_dir = Path(self._answers["work_dir"]).expanduser() + for src in _TEMPLATES_DIR.iterdir(): + if src.is_file(): + shutil.copy2(src, work_dir / src.name) + self.mark_done("copy_files") + + def _write_tfvars(self) -> None: + if self.already_done("write_tfvars"): + return + work_dir = Path(self._answers["work_dir"]).expanduser() + api_key = self._kwargs.get("cortex_api_key", "") + base_url = self._kwargs.get("cortex_base_url", "https://api.getcortexapp.com") + tfvars = work_dir / "terraform.tfvars" + tfvars.write_text( + f'cortex_api_token = "{api_key}"\n' + f'cortex_base_url = "{base_url}"\n' + ) + self.mark_done("write_tfvars") + + def _write_gitignore(self) -> None: + if self.already_done("write_gitignore"): + return + work_dir = Path(self._answers["work_dir"]).expanduser() + gitignore = work_dir / ".gitignore" + existing = gitignore.read_text() if gitignore.exists() else "" + additions = [e for e in _GITIGNORE_ENTRIES if e not in existing] + if additions: + with gitignore.open("a") as f: + if existing and not existing.endswith("\n"): + f.write("\n") + f.write("\n".join(additions) + "\n") + self.mark_done("write_gitignore") + + def _terraform_init(self) -> None: + if self.already_done("terraform_init"): + return + work_dir = Path(self._answers["work_dir"]).expanduser() + result = subprocess.run( + ["terraform", "init"], + cwd=work_dir, + capture_output=False, # stream output to terminal + ) + if result.returncode != 0: + raise RuntimeError("terraform init failed — see output above") + self.mark_done("terraform_init") + + def _terraform_apply(self) -> None: + if self.already_done("terraform_apply"): + return + work_dir = Path(self._answers["work_dir"]).expanduser() + result = subprocess.run( + ["terraform", "apply", "-auto-approve"], + cwd=work_dir, + capture_output=False, # stream output to terminal + ) + if result.returncode != 0: + raise RuntimeError( + "terraform apply failed — see output above.\n" + f"State may be partially created. Retry from: {work_dir}" + ) + self.mark_done("terraform_apply") + + +def main(**kwargs): + TerraformSetup(**kwargs).run() +``` + +> **Note on `self._answers` and `self._kwargs`:** Check `setup_base.py` to confirm the attribute names for stored prompt answers and kwargs. From reading `github-actions-deploy/setup.py`, answers are accessed via `self._answers["key"]` and kwargs via `self._kwargs`. If the base class uses different names, adjust accordingly. + +- [ ] **Step 2: Verify the module is importable** + +```bash +poetry run python -c "from cortexapps_cli.solutions.terraform.setup import main; print('OK')" +``` + +Expected: `OK` + +- [ ] **Step 3: Commit** + +```bash +git add cortexapps_cli/solutions/terraform/setup.py +git commit -m "feat: add setup.py for terraform solution (CX-43)" +``` + +--- + +## Task 9: End-to-end test + +**Files:** None created — this is a manual verification task. + +- [ ] **Step 1: Verify solution appears in list and info** + +```bash +poetry run cortex solutions list +poetry run cortex solutions info -s terraform +``` + +Expected: `list` shows Terraform row; `info` shows full README content. + +- [ ] **Step 2: Verify terraform template files are syntactically valid** + +If you have Terraform installed locally: + +```bash +cd /tmp && mkdir tf-validate && cp cortexapps_cli/solutions/terraform/_templates/terraform/*.tf tf-validate/ +# Create a minimal tfvars so validate doesn't error on missing required vars +echo 'cortex_api_token = "fake"' > tf-validate/terraform.tfvars +cd tf-validate && terraform init && terraform validate +``` + +Expected: `Success! The configuration is valid.` + +If `terraform validate` reports schema errors (unknown attribute, incorrect type, etc.): +1. Open https://registry.terraform.io/providers/cortexapps/cortex/latest/docs/resources/catalog_entity in a browser +2. Find the correct attribute name +3. Update the affected `.tf` files in both `_templates/terraform/` and `_templates/terraform-delta/` + +- [ ] **Step 3: Run the full post-install against a real Cortex instance** + +Requires `CORTEX_API_KEY` to be set. + +```bash +poetry run cortex solutions post-install -s terraform +``` + +Accept the default working directory. Watch `terraform init` download the provider and `terraform apply` create entities. + +- [ ] **Step 4: Verify entities in Cortex** + +In the Cortex UI, filter catalog by group `terraform-demo`. You should see: +- 4 teams +- 2 domains +- 6 services +- All 6 services at Bronze on Production Readiness scorecard + +- [ ] **Step 5: Apply the delta** + +```bash +cp cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf ~/parts-unlimited-terraform/ecommerce.tf +cp cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf ~/parts-unlimited-terraform/teams.tf +cd ~/parts-unlimited-terraform && terraform plan +``` + +Confirm plan output shows: +- `~ cortex_catalog_entity.phoenix` (update) +- `+ cortex_catalog_entity.notification_service` (create) +- `~ cortex_catalog_entity.team_development` (update) + +Then apply: + +```bash +terraform apply +``` + +- [ ] **Step 6: Verify scorecard promotion** + +Open Production Readiness scorecard in Cortex. The Phoenix Project should now show **Silver**. + +- [ ] **Step 7: Final commit if any fixes were needed** + +```bash +git add -p # stage only intentional changes +git commit -m "fix: correct terraform HCL field names after validation (CX-43)" +``` From d7775d69dfd84698789b24e9dfc2f1ed9eed4e7c Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:20:33 -0700 Subject: [PATCH 04/40] feat: add terraform solution scaffold and README (CX-43) --- cortexapps_cli/solutions/terraform/README.md | 200 +++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 cortexapps_cli/solutions/terraform/README.md diff --git a/cortexapps_cli/solutions/terraform/README.md b/cortexapps_cli/solutions/terraform/README.md new file mode 100644 index 00000000..1bf38635 --- /dev/null +++ b/cortexapps_cli/solutions/terraform/README.md @@ -0,0 +1,200 @@ +--- +name: Terraform +description: Manage your Cortex catalog as code using the Cortex Terraform provider. +--- + +# Terraform + +The [Cortex Terraform provider](https://github.com/cortexapps/terraform-provider-cortex) lets you define your entire service catalog — teams, services, domains, scorecards — as HCL code in `.tf` files. Changes go through PR review and apply automatically on merge, giving you a fully auditable, GitOps-driven catalog. + +## What is HCL? + +HCL (HashiCorp Configuration Language) is the declarative language used in `.tf` files. It reads like structured config rather than code: + +```hcl +resource "cortex_catalog_entity" "phoenix" { + tag = "phoenix" + name = "The Phoenix Project" + description = "Main e-commerce monolith for Parts Unlimited." + + owners = [{ name = "team-development", type = "group", provider = "CORTEX" }] + + git = { + github = { repository = "parts-unlimited/phoenix" } + } +} +``` + +Terraform reads all `.tf` files in a directory, compares them to the current live state, and applies only what changed. + +## Terraform vs. cortex.yaml — two flavors of GitOps + +Both approaches keep your catalog in git and apply changes on merge. The difference is where the files live: + +| | Terraform | cortex.yaml | +|---|---|---| +| **Files live in** | One central infra repo | Each service's own repo | +| **Managed by** | Platform / infra team | Individual service teams | +| **Also manages** | AWS, GCP, everything else | Just Cortex catalog | +| **Apply mechanism** | CI runs `terraform apply` | Cortex git integration polls files | + +Neither is better. Platform teams who already manage cloud infrastructure with Terraform often prefer the centralized approach. Dev teams who want catalog config alongside their code prefer cortex.yaml. + +## File ownership at scale + +Terraform has no requirements on file names — it reads all `.tf` files in a directory and merges them. This means you can split by team or domain ownership: + +``` +infra-catalog/ +├── provider.tf ← platform team +├── teams.tf ← platform team +├── scorecards.tf ← platform team +├── ecommerce.tf ← e-commerce team (domain + services) +├── supply-chain.tf ← supply chain team (domain + services) +└── payments.tf ← payments team +``` + +Each team submits PRs touching only their file. At very large scale (100K+ services), teams use Terraform modules (subdirectories) to organize further — but the ownership model is the same. + +## What's Included + +This solution installs a demo org for **Parts Unlimited** (from *The Phoenix Project*): + +- **4 teams**: Development (Bill's team), IT Operations (Brent's domain), Information Security (John's team), QA +- **2 domains**: E-Commerce, Supply Chain +- **6 services**: The Phoenix Project, Parts Catalog API, Payments Service, Inventory Service, Ordering Service, Shipping Service +- **1 scorecard**: Production Readiness (Bronze/Silver/Gold) + +All services start at **Bronze** — intentionally incomplete so you can see the delta in action. + +## Prerequisites + +- Terraform >= 1.5 — install at https://developer.hashicorp.com/terraform/install +- Cortex API key with write access + +## Install + +```bash +cortex solutions post-install -s terraform +``` + +You'll be asked where to create the working directory (default: `~/parts-unlimited-terraform`). The script will: +1. Verify Terraform is installed +2. Copy the `.tf` files to your working directory +3. Write your credentials to `terraform.tfvars` +4. Run `terraform init` to download the Cortex provider +5. Run `terraform apply` to create all entities in Cortex + +## Explore what was created + +After install, browse your Cortex catalog filtered by the `terraform-demo` group to see all created entities. Open the **Production Readiness** scorecard to see all 6 services at Bronze. + +## Try the Delta + +The delta shows what a real team PR looks like — modify a file, plan, apply, watch the scorecard update. + +**Step 1: Copy the delta files into your working directory** + +```bash +cp /terraform-delta/ecommerce.tf ~/parts-unlimited-terraform/ecommerce.tf +cp /terraform-delta/teams.tf ~/parts-unlimited-terraform/teams.tf +``` + +> The delta files are in `_templates/terraform-delta/` inside the installed solutions package. Run `cortex solutions info -s terraform` to find the exact path. + +**Step 2: See what will change** + +```bash +cd ~/parts-unlimited-terraform +terraform plan +``` + +You'll see: +- `~ cortex_catalog_entity.phoenix` — **update** (adds links, metadata) +- `+ cortex_catalog_entity.notification_service` — **create** (new service) +- `~ cortex_catalog_entity.team_development` — **update** (new team member) + +**Step 3: Apply** + +```bash +terraform apply +``` + +**Step 4: Check the scorecard** + +Open Production Readiness in Cortex. The Phoenix Project should now show **Silver**. + +## File Walkthrough + +**`provider.tf`** — Declares the `cortexapps/cortex` provider version and reads credentials from variables. This is the only file that changes if you upgrade the provider version. + +**`variables.tf`** — Defines `cortex_api_token` (sensitive) and `cortex_base_url`. Values come from `terraform.tfvars` (never committed) or environment variables (`CORTEX_API_TOKEN`, `CORTEX_API_URL`). + +**`teams.tf`** — Owned by the platform team. Defines all teams and their members. Changes here require a platform PR. + +**`ecommerce.tf`** — Owned by the e-commerce team. Defines the E-Commerce domain and its three services. Changes here — new services, updated descriptions, added links — are the e-commerce team's PR to make. + +**`supply-chain.tf`** — Same pattern, owned by the supply chain team. + +**`scorecards.tf`** — Owned by the platform team. Defines the Production Readiness scorecard and its Bronze/Silver/Gold rules. + +## Customizing for Your Org + +1. Rename entity tags and display names throughout +2. Replace `parts-unlimited/*` GitHub repos with your actual repos +3. Add more services: copy any service block from `ecommerce.tf` and adjust the tag, name, and owner +4. Extend scorecard rules: add rules to `scorecards.tf` with the expression language shown in the [Cortex docs](https://docs.cortex.io/docs/reference/scorecard-rules) +5. Split into more files as your team grows — Terraform reads them all + +## Next Steps: CI/CD Integration + +In practice, customers commit their `.tf` files to a repo and let CI handle applies. Here's a minimal GitHub Actions workflow: + +```yaml +# .github/workflows/cortex-catalog.yml +name: Cortex Catalog + +on: + pull_request: + paths: ['catalog/**'] + push: + branches: [main] + paths: ['catalog/**'] + +jobs: + plan: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: "~> 1.5" + - run: terraform init + working-directory: catalog + env: + TF_VAR_cortex_api_token: ${{ secrets.CORTEX_API_TOKEN }} + - run: terraform plan + working-directory: catalog + env: + TF_VAR_cortex_api_token: ${{ secrets.CORTEX_API_TOKEN }} + + apply: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: "~> 1.5" + - run: terraform init + working-directory: catalog + env: + TF_VAR_cortex_api_token: ${{ secrets.CORTEX_API_TOKEN }} + - run: terraform apply -auto-approve + working-directory: catalog + env: + TF_VAR_cortex_api_token: ${{ secrets.CORTEX_API_TOKEN }} +``` + +Store `CORTEX_API_TOKEN` as a GitHub Actions secret. Now every PR shows a plan diff as a CI check, and every merge to main applies automatically. From cd0fe59fb458b9c6060da88f3d23c02c4e2c17e6 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:22:11 -0700 Subject: [PATCH 05/40] feat: add terraform provider config files (CX-43) --- .../terraform/_templates/terraform/provider.tf | 14 ++++++++++++++ .../_templates/terraform/terraform.tfvars.example | 10 ++++++++++ .../terraform/_templates/terraform/variables.tf | 11 +++++++++++ 3 files changed, 35 insertions(+) create mode 100644 cortexapps_cli/solutions/terraform/_templates/terraform/provider.tf create mode 100644 cortexapps_cli/solutions/terraform/_templates/terraform/terraform.tfvars.example create mode 100644 cortexapps_cli/solutions/terraform/_templates/terraform/variables.tf diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/provider.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/provider.tf new file mode 100644 index 00000000..4f57e942 --- /dev/null +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/provider.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + cortex = { + source = "cortexapps/cortex" + version = "~> 0.6" + } + } + required_version = ">= 1.5" +} + +provider "cortex" { + token = var.cortex_api_token + base_api_url = var.cortex_base_url +} diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/terraform.tfvars.example b/cortexapps_cli/solutions/terraform/_templates/terraform/terraform.tfvars.example new file mode 100644 index 00000000..dfdec40b --- /dev/null +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/terraform.tfvars.example @@ -0,0 +1,10 @@ +# Copy this file to terraform.tfvars and fill in your values. +# IMPORTANT: Never commit terraform.tfvars to source control — it contains your API token. +# Add terraform.tfvars to your .gitignore. + +# Your Cortex API token. +# Alternatively, set the CORTEX_API_TOKEN environment variable and omit this line. +# cortex_api_token = "your-api-token-here" + +# Cortex API base URL. Only change if you are on a self-hosted instance. +cortex_base_url = "https://api.getcortexapp.com" diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/variables.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/variables.tf new file mode 100644 index 00000000..1d6f1504 --- /dev/null +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/variables.tf @@ -0,0 +1,11 @@ +variable "cortex_api_token" { + description = "Cortex API token. Can also be set via the CORTEX_API_TOKEN environment variable." + type = string + sensitive = true +} + +variable "cortex_base_url" { + description = "Cortex API base URL." + type = string + default = "https://api.getcortexapp.com" +} From 3cd5911ebd2aa811afaaf65cdd91fa1e54b72809 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:22:23 -0700 Subject: [PATCH 06/40] feat: add teams.tf for terraform solution (CX-43) --- .../terraform/_templates/terraform/teams.tf | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf new file mode 100644 index 00000000..6edfa186 --- /dev/null +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf @@ -0,0 +1,96 @@ +# teams.tf — Platform-owned +# Changes to teams (membership, new hires, reorgs) are made here via platform PR. + +resource "cortex_catalog_entity" "team_development" { + tag = "team-development" + name = "Development" + description = "Application development team responsible for Parts Unlimited's e-commerce platform and core services." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "Bill Palmer" + email = "bill.palmer@parts-unlimited.com" + role = "Team Lead" + description = "IT Manager leading the Phoenix Project" + }, + { + name = "Maxine Chambers" + email = "maxine.chambers@parts-unlimited.com" + role = "Senior Engineer" + description = "Staff engineer on the Phoenix Project" + }, + { + name = "Dev Magee" + email = "dev.magee@parts-unlimited.com" + role = "Engineer" + description = "Developer on the Phoenix Project" + } + ] + } +} + +resource "cortex_catalog_entity" "team_operations" { + tag = "team-operations" + name = "IT Operations" + description = "Infrastructure, reliability, and operations for Parts Unlimited's production systems." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "Brent Geller" + email = "brent.geller@parts-unlimited.com" + role = "Principal Engineer" + description = "Indispensable operations expert and bottleneck" + }, + { + name = "Wes Davis" + email = "wes.davis@parts-unlimited.com" + role = "Operations Manager" + description = "Manages day-to-day operations work" + } + ] + } +} + +resource "cortex_catalog_entity" "team_security" { + tag = "team-security" + name = "Information Security" + description = "Security, compliance, and risk management for Parts Unlimited." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "John Pesche" + email = "john.pesche@parts-unlimited.com" + role = "CISO" + description = "Chief Information Security Officer" + } + ] + } +} + +resource "cortex_catalog_entity" "team_qa" { + tag = "team-qa" + name = "Quality Assurance" + description = "Testing, QA, and release verification for Parts Unlimited services." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "Patty McKee" + email = "patty.mckee@parts-unlimited.com" + role = "QA Manager" + description = "Manages QA processes and testing" + } + ] + } +} From 35e82d53c9a9a685e6b1730a7fed0955f4cf27ed Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:22:33 -0700 Subject: [PATCH 07/40] feat: add ecommerce.tf for terraform solution (CX-43) --- .../_templates/terraform/ecommerce.tf | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf new file mode 100644 index 00000000..b81d3972 --- /dev/null +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf @@ -0,0 +1,84 @@ +# ecommerce.tf — E-Commerce team-owned +# This file defines the E-Commerce domain and all services within it. +# The e-commerce team submits PRs to this file to add/update services. +# +# NOTE: Services are intentionally at Bronze level only (no links, no metadata). +# See _templates/terraform-delta/ecommerce.tf for the Silver-state version. + +resource "cortex_catalog_entity" "domain_ecommerce" { + tag = "domain-ecommerce" + name = "E-Commerce" + description = "Customer-facing e-commerce platform including product catalog, checkout, and payments." + type = "domain" + + groups = ["terraform-demo"] +} + +resource "cortex_catalog_entity" "phoenix" { + tag = "phoenix" + name = "The Phoenix Project" + description = "Main e-commerce monolith handling browsing and checkout." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/phoenix" + base_path = "/" + } + } +} + +resource "cortex_catalog_entity" "parts_catalog_api" { + tag = "parts-catalog-api" + name = "Parts Catalog API" + description = "REST API for browsing the parts catalog." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/parts-catalog-api" + base_path = "/" + } + } +} + +resource "cortex_catalog_entity" "payments_service" { + tag = "payments-service" + name = "Payments Service" + description = "Payment processing and refund handling." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/payments-service" + base_path = "/" + } + } +} From e53be4ab088ae5cb66e8d736b199546ef21d3ec8 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:22:42 -0700 Subject: [PATCH 08/40] feat: add supply-chain.tf for terraform solution (CX-43) --- .../_templates/terraform/supply-chain.tf | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf new file mode 100644 index 00000000..964b12bb --- /dev/null +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf @@ -0,0 +1,81 @@ +# supply-chain.tf — Supply Chain team-owned +# This file defines the Supply Chain domain and all services within it. +# The supply chain team submits PRs to this file to add/update services. + +resource "cortex_catalog_entity" "domain_supply_chain" { + tag = "domain-supply-chain" + name = "Supply Chain" + description = "Inventory, ordering, and shipping services supporting Parts Unlimited's fulfillment operations." + type = "domain" + + groups = ["terraform-demo"] +} + +resource "cortex_catalog_entity" "inventory_service" { + tag = "inventory-service" + name = "Inventory Service" + description = "Real-time inventory tracking across all Parts Unlimited warehouses." + + owners = [ + { + name = "team-operations" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:supply-chain"] + + git = { + github = { + repository = "parts-unlimited/inventory-service" + base_path = "/" + } + } +} + +resource "cortex_catalog_entity" "ordering_service" { + tag = "ordering-service" + name = "Ordering Service" + description = "Order placement, validation, and fulfillment coordination." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:supply-chain"] + + git = { + github = { + repository = "parts-unlimited/ordering-service" + base_path = "/" + } + } +} + +resource "cortex_catalog_entity" "shipping_service" { + tag = "shipping-service" + name = "Shipping Service" + description = "Shipping and logistics tracking for Parts Unlimited orders." + + owners = [ + { + name = "team-operations" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:supply-chain"] + + git = { + github = { + repository = "parts-unlimited/shipping-service" + base_path = "/" + } + } +} From 6427bca840bd1b17df10447dc2fe684f5ef9122b Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:22:56 -0700 Subject: [PATCH 09/40] feat: add scorecards.tf for terraform solution (CX-43) --- .../_templates/terraform/scorecards.tf | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf new file mode 100644 index 00000000..cbd5d7ee --- /dev/null +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -0,0 +1,113 @@ +# scorecards.tf — Platform-owned +# Defines the Production Readiness scorecard. +# Bronze: automatically achieved by all properly-defined services. +# Silver: requires adding links and metadata — see the delta. +# Gold: requires shared ownership and a rich description — aspirational. + +resource "cortex_scorecard" "production_readiness" { + tag = "production-readiness" + name = "Production Readiness" + description = "Measures how production-ready a Parts Unlimited service is. Bronze is table stakes; Gold is the aspirational standard." + draft = false + + ladder = { + levels = [ + { + name = "Gold" + rank = 3 + color = "#D7AC58" + }, + { + name = "Silver" + rank = 2 + color = "#C0C0C0" + }, + { + name = "Bronze" + rank = 1 + color = "#CD7F32" + } + ] + } + + rules = [ + # ── Bronze ──────────────────────────────────────────────────────────────── + { + title = "Has description" + description = "Service must have a non-empty description." + expression = "entity.description().length > 0" + weight = 1 + level = "Bronze" + }, + { + title = "Has owner team" + description = "Service must be owned by at least one team." + expression = "owners.teams.size() > 0" + weight = 1 + level = "Bronze" + }, + { + title = "Has git configured" + description = "Service must have a git repository linked." + expression = "git != null" + weight = 1 + level = "Bronze" + }, + + # ── Silver ──────────────────────────────────────────────────────────────── + { + title = "Has at least one link" + description = "Service must have at least one link (runbook, docs, dashboard, etc.)." + expression = "links.size() > 0" + weight = 1 + level = "Silver" + }, + { + title = "Has terraform-workspace metadata" + description = "Service must declare its Terraform workspace via the terraform-workspace metadata key." + expression = "customData.exists(d, d.key == \"terraform-workspace\")" + weight = 1 + level = "Silver" + }, + { + title = "Meaningful description" + description = "Service description should be at least 30 characters." + expression = "entity.description().length >= 30" + weight = 1 + level = "Silver" + }, + + # ── Gold ────────────────────────────────────────────────────────────────── + { + title = "Rich description" + description = "Service description should be at least 50 characters." + expression = "entity.description().length >= 50" + weight = 1 + level = "Gold" + }, + { + title = "Shared ownership" + description = "Critical services should be owned by at least two teams to avoid single points of knowledge." + expression = "owners.teams.size() >= 2" + weight = 1 + level = "Gold" + }, + { + title = "Has runbook" + description = "Service must have a runbook link for on-call responders." + expression = "links.exists(l, l.type == \"runbook\")" + weight = 1 + level = "Gold" + } + ] + + filter = { + types = { + include = ["service"] + } + } + + evaluation = { + window = 24 + } +} From 2f88b46e27b8d53fcfa1b20b9cebaa2c4fcf7fa2 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:25:22 -0700 Subject: [PATCH 10/40] feat: add terraform-delta files for terraform solution (CX-43) --- .../_templates/terraform-delta/ecommerce.tf | 125 ++++++++++++++++++ .../_templates/terraform-delta/teams.tf | 104 +++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf create mode 100644 cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf new file mode 100644 index 00000000..8c30426d --- /dev/null +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf @@ -0,0 +1,125 @@ +# ecommerce.tf — E-Commerce team-owned (DELTA VERSION) +# This is a complete replacement for _templates/terraform/ecommerce.tf +# Changes: phoenix promoted to Silver (links + metadata + expanded description), +# notification-service added (new Bronze service) + +resource "cortex_catalog_entity" "domain_ecommerce" { + tag = "domain-ecommerce" + name = "E-Commerce" + description = "Customer-facing e-commerce platform including product catalog, checkout, and payments." + type = "domain" + + groups = ["terraform-demo"] +} + +# CHANGED: description expanded (≥30 chars for Silver rule 3), +# links added (Silver rule 1), metadata added (Silver rule 2) +resource "cortex_catalog_entity" "phoenix" { + tag = "phoenix" + name = "The Phoenix Project" + description = "Main e-commerce monolith for Parts Unlimited, handling product browsing, cart, and checkout flows." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/phoenix" + base_path = "/" + } + } + + links = [ + { + name = "Runbook" + type = "runbook" + url = "https://wiki.parts-unlimited.com/runbooks/phoenix" + }, + { + name = "Architecture Docs" + type = "documentation" + url = "https://wiki.parts-unlimited.com/architecture/phoenix" + } + ] + + metadata = jsonencode({ + "terraform-workspace" = "phoenix-prod" + }) +} + +resource "cortex_catalog_entity" "parts_catalog_api" { + tag = "parts-catalog-api" + name = "Parts Catalog API" + description = "REST API for browsing the parts catalog." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/parts-catalog-api" + base_path = "/" + } + } +} + +resource "cortex_catalog_entity" "payments_service" { + tag = "payments-service" + name = "Payments Service" + description = "Payment processing and refund handling." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/payments-service" + base_path = "/" + } + } +} + +# NEW SERVICE — will show as `+ create` in terraform plan +resource "cortex_catalog_entity" "notification_service" { + tag = "notification-service" + name = "Notification Service" + description = "Handles email, SMS, and push notifications for Parts Unlimited customer events." + + owners = [ + { + name = "team-development" + type = "group" + provider = "CORTEX" + } + ] + + groups = ["terraform-demo", "domain:ecommerce"] + + git = { + github = { + repository = "parts-unlimited/notification-service" + base_path = "/" + } + } +} diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf new file mode 100644 index 00000000..f58a1786 --- /dev/null +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf @@ -0,0 +1,104 @@ +# teams.tf — Platform-owned (DELTA VERSION) +# This is a complete replacement for _templates/terraform/teams.tf +# Changes: Sarah Connor added to team-development + +# CHANGED: Sarah Connor added +resource "cortex_catalog_entity" "team_development" { + tag = "team-development" + name = "Development" + description = "Application development team responsible for Parts Unlimited's e-commerce platform and core services." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "Bill Palmer" + email = "bill.palmer@parts-unlimited.com" + role = "Team Lead" + description = "IT Manager leading the Phoenix Project" + }, + { + name = "Maxine Chambers" + email = "maxine.chambers@parts-unlimited.com" + role = "Senior Engineer" + description = "Staff engineer on the Phoenix Project" + }, + { + name = "Dev Magee" + email = "dev.magee@parts-unlimited.com" + role = "Engineer" + description = "Developer on the Phoenix Project" + }, + { + name = "Sarah Connor" + email = "sarah.connor@parts-unlimited.com" + role = "Engineer" + description = "New hire joining the Phoenix Project team" + } + ] + } +} + +resource "cortex_catalog_entity" "team_operations" { + tag = "team-operations" + name = "IT Operations" + description = "Infrastructure, reliability, and operations for Parts Unlimited's production systems." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "Brent Geller" + email = "brent.geller@parts-unlimited.com" + role = "Principal Engineer" + description = "Indispensable operations expert and bottleneck" + }, + { + name = "Wes Davis" + email = "wes.davis@parts-unlimited.com" + role = "Operations Manager" + description = "Manages day-to-day operations work" + } + ] + } +} + +resource "cortex_catalog_entity" "team_security" { + tag = "team-security" + name = "Information Security" + description = "Security, compliance, and risk management for Parts Unlimited." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "John Pesche" + email = "john.pesche@parts-unlimited.com" + role = "CISO" + description = "Chief Information Security Officer" + } + ] + } +} + +resource "cortex_catalog_entity" "team_qa" { + tag = "team-qa" + name = "Quality Assurance" + description = "Testing, QA, and release verification for Parts Unlimited services." + + groups = ["terraform-demo"] + + team = { + members = [ + { + name = "Patty McKee" + email = "patty.mckee@parts-unlimited.com" + role = "QA Manager" + description = "Manages QA processes and testing" + } + ] + } +} From 1e6398a9d607621d656c77a08d667e533f00a502 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:27:34 -0700 Subject: [PATCH 11/40] feat: add setup.py for terraform solution (CX-43) Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/solutions/terraform/setup.py | 199 ++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 cortexapps_cli/solutions/terraform/setup.py diff --git a/cortexapps_cli/solutions/terraform/setup.py b/cortexapps_cli/solutions/terraform/setup.py new file mode 100644 index 00000000..2afe4e40 --- /dev/null +++ b/cortexapps_cli/solutions/terraform/setup.py @@ -0,0 +1,199 @@ +""" +Post-install setup for the terraform solution. +Verifies Terraform is installed, copies template files to a working directory, +writes credentials, and runs terraform init + apply. + +Run via: cortex solutions post-install -s terraform +""" + +SETUP_DESCRIPTION = ( + "Sets up the Parts Unlimited demo org in your Cortex instance using the " + "Cortex Terraform provider. Requires Terraform >= 1.5 — install at " + "https://developer.hashicorp.com/terraform/install" +) + +import json +import shutil +import subprocess +import sys +from pathlib import Path + +try: + from cortexapps_cli.solutions._lib.setup_base import SolutionSetup +except ImportError: + sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + from _lib.setup_base import SolutionSetup + +_TEMPLATES_DIR = Path(__file__).parent / "_templates" / "terraform" +_DELTA_DIR = Path(__file__).parent / "_templates" / "terraform-delta" + +_GITIGNORE_ENTRIES = [ + "terraform.tfvars", + ".terraform/", + "*.tfstate", + "*.tfstate.backup", + ".terraform.lock.hcl", +] + + +class TerraformSetup(SolutionSetup): + solution_tag = "terraform" + + def __init__(self, cortex_api_key: str = None, cortex_base_url: str = None, no_prompt: bool = False, **kwargs): + super().__init__(no_prompt=no_prompt, **kwargs) + self._session_api_key = cortex_api_key + self._session_base_url = cortex_base_url + + def collect_prompts(self) -> None: + self.prompt( + "work_dir", + "Working directory for Terraform files", + default=str(Path.home() / "parts-unlimited-terraform"), + env_var="TERRAFORM_WORK_DIR", + ) + + def steps(self) -> list[tuple[str, callable]]: + return [ + ("Check Terraform CLI", self._check_terraform), + ("Create working directory", self._create_work_dir), + ("Copy Terraform files", self._copy_files), + ("Write terraform.tfvars", self._write_tfvars), + ("Write .gitignore", self._write_gitignore), + ("terraform init", self._terraform_init), + ("terraform apply", self._terraform_apply), + ] + + def post_steps(self) -> None: + work_dir = self._answers["work_dir"] + delta_dir = _DELTA_DIR + + print("\n✓ Parts Unlimited demo org created in Cortex via Terraform!\n") + print(f" Terraform files are at: {work_dir}\n") + print("─" * 60) + print("NEXT: Try the delta to see Terraform's incremental update\n") + print(" 1. Copy the delta files into your working directory:") + print(f" cp {delta_dir}/ecommerce.tf {work_dir}/ecommerce.tf") + print(f" cp {delta_dir}/teams.tf {work_dir}/teams.tf\n") + print(" 2. Preview the changes:") + print(f" cd {work_dir} && terraform plan\n") + print(" Look for:") + print(" ~ cortex_catalog_entity.phoenix (update: links + metadata added)") + print(" + cortex_catalog_entity.notification_service (create: new service)") + print(" ~ cortex_catalog_entity.team_development (update: new member)\n") + print(" 3. Apply:") + print(f" terraform apply\n") + print(" 4. Check the Production Readiness scorecard in Cortex.") + print(" The Phoenix Project should now show Silver.\n") + print("─" * 60) + print("To use Terraform for your real catalog, see the CI/CD integration") + print("section in: cortex solutions info -s terraform") + + # ── Private step implementations ────────────────────────────────────────── + + def _check_terraform(self) -> None: + if self.already_done("check_terraform"): + return + result = shutil.which("terraform") + if result is None: + print( + "\nERROR: terraform CLI not found in PATH.\n" + "Install Terraform >= 1.5 from: https://developer.hashicorp.com/terraform/install", + file=sys.stderr, + ) + raise RuntimeError("terraform not found") + # Check version >= 1.5 + try: + out = subprocess.check_output( + ["terraform", "version", "-json"], text=True + ) + version_str = json.loads(out).get("terraform_version", "0.0.0") + major, minor, *_ = (int(x) for x in version_str.split(".")) + if (major, minor) < (1, 5): + raise RuntimeError( + f"Terraform {version_str} is too old. Version >= 1.5 required.\n" + "Upgrade at: https://developer.hashicorp.com/terraform/install" + ) + except (subprocess.CalledProcessError, KeyError, ValueError): + # If version check fails, proceed — let terraform itself error if needed + pass + self.mark_done("check_terraform") + + def _create_work_dir(self) -> None: + if self.already_done("create_work_dir"): + return + work_dir = Path(self._answers["work_dir"]).expanduser() + work_dir.mkdir(parents=True, exist_ok=True) + self.mark_done("create_work_dir") + + def _copy_files(self) -> None: + if self.already_done("copy_files"): + return + work_dir = Path(self._answers["work_dir"]).expanduser() + for src in _TEMPLATES_DIR.iterdir(): + if src.is_file(): + shutil.copy2(src, work_dir / src.name) + self.mark_done("copy_files") + + def _write_tfvars(self) -> None: + if self.already_done("write_tfvars"): + return + work_dir = Path(self._answers["work_dir"]).expanduser() + api_key = self._session_api_key or "" + base_url = self._session_base_url or "https://api.getcortexapp.com" + tfvars = work_dir / "terraform.tfvars" + tfvars.write_text( + f'cortex_api_token = "{api_key}"\n' + f'cortex_base_url = "{base_url}"\n' + ) + self.mark_done("write_tfvars") + + def _write_gitignore(self) -> None: + if self.already_done("write_gitignore"): + return + work_dir = Path(self._answers["work_dir"]).expanduser() + gitignore = work_dir / ".gitignore" + existing = gitignore.read_text() if gitignore.exists() else "" + additions = [e for e in _GITIGNORE_ENTRIES if e not in existing] + if additions: + with gitignore.open("a") as f: + if existing and not existing.endswith("\n"): + f.write("\n") + f.write("\n".join(additions) + "\n") + self.mark_done("write_gitignore") + + def _terraform_init(self) -> None: + if self.already_done("terraform_init"): + return + work_dir = Path(self._answers["work_dir"]).expanduser() + result = subprocess.run( + ["terraform", "init"], + cwd=work_dir, + capture_output=False, # stream output to terminal + ) + if result.returncode != 0: + raise RuntimeError("terraform init failed — see output above") + self.mark_done("terraform_init") + + def _terraform_apply(self) -> None: + if self.already_done("terraform_apply"): + return + work_dir = Path(self._answers["work_dir"]).expanduser() + result = subprocess.run( + ["terraform", "apply", "-auto-approve"], + cwd=work_dir, + capture_output=False, # stream output to terminal + ) + if result.returncode != 0: + raise RuntimeError( + "terraform apply failed — see output above.\n" + f"State may be partially created. Retry from: {work_dir}" + ) + self.mark_done("terraform_apply") + + +def main(**kwargs): + TerraformSetup(**kwargs).run() + + +if __name__ == "__main__": + main() From c2b6efcd41295b2077ba0e8d37102b40d64ba1e8 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:32:42 -0700 Subject: [PATCH 12/40] fix: correct team entity type and README cleanup (CX-43) Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/solutions/terraform/README.md | 9 ++------- .../terraform/_templates/terraform-delta/teams.tf | 4 ++++ .../solutions/terraform/_templates/terraform/teams.tf | 4 ++++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/README.md b/cortexapps_cli/solutions/terraform/README.md index 1bf38635..68392092 100644 --- a/cortexapps_cli/solutions/terraform/README.md +++ b/cortexapps_cli/solutions/terraform/README.md @@ -95,12 +95,7 @@ The delta shows what a real team PR looks like — modify a file, plan, apply, w **Step 1: Copy the delta files into your working directory** -```bash -cp /terraform-delta/ecommerce.tf ~/parts-unlimited-terraform/ecommerce.tf -cp /terraform-delta/teams.tf ~/parts-unlimited-terraform/teams.tf -``` - -> The delta files are in `_templates/terraform-delta/` inside the installed solutions package. Run `cortex solutions info -s terraform` to find the exact path. +The exact delta file paths were printed when you ran `cortex solutions post-install -s terraform`. Copy the delta files shown there to your working directory. **Step 2: See what will change** @@ -122,7 +117,7 @@ terraform apply **Step 4: Check the scorecard** -Open Production Readiness in Cortex. The Phoenix Project should now show **Silver**. +Open Production Readiness in Cortex. The Phoenix Project should now show **Silver**. Also check that the new **Notification Service** appears in your catalog. ## File Walkthrough diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf index f58a1786..7c5bfd2d 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf @@ -5,6 +5,7 @@ # CHANGED: Sarah Connor added resource "cortex_catalog_entity" "team_development" { tag = "team-development" + type = "team" name = "Development" description = "Application development team responsible for Parts Unlimited's e-commerce platform and core services." @@ -42,6 +43,7 @@ resource "cortex_catalog_entity" "team_development" { resource "cortex_catalog_entity" "team_operations" { tag = "team-operations" + type = "team" name = "IT Operations" description = "Infrastructure, reliability, and operations for Parts Unlimited's production systems." @@ -67,6 +69,7 @@ resource "cortex_catalog_entity" "team_operations" { resource "cortex_catalog_entity" "team_security" { tag = "team-security" + type = "team" name = "Information Security" description = "Security, compliance, and risk management for Parts Unlimited." @@ -86,6 +89,7 @@ resource "cortex_catalog_entity" "team_security" { resource "cortex_catalog_entity" "team_qa" { tag = "team-qa" + type = "team" name = "Quality Assurance" description = "Testing, QA, and release verification for Parts Unlimited services." diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf index 6edfa186..e42fc812 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf @@ -3,6 +3,7 @@ resource "cortex_catalog_entity" "team_development" { tag = "team-development" + type = "team" name = "Development" description = "Application development team responsible for Parts Unlimited's e-commerce platform and core services." @@ -34,6 +35,7 @@ resource "cortex_catalog_entity" "team_development" { resource "cortex_catalog_entity" "team_operations" { tag = "team-operations" + type = "team" name = "IT Operations" description = "Infrastructure, reliability, and operations for Parts Unlimited's production systems." @@ -59,6 +61,7 @@ resource "cortex_catalog_entity" "team_operations" { resource "cortex_catalog_entity" "team_security" { tag = "team-security" + type = "team" name = "Information Security" description = "Security, compliance, and risk management for Parts Unlimited." @@ -78,6 +81,7 @@ resource "cortex_catalog_entity" "team_security" { resource "cortex_catalog_entity" "team_qa" { tag = "team-qa" + type = "team" name = "Quality Assurance" description = "Testing, QA, and release verification for Parts Unlimited services." From 5b101b59558de59f747602ca4cd02dff0d3bc664 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:45:33 -0700 Subject: [PATCH 13/40] fix: correct team member schema and scorecard expressions (CX-43) Co-Authored-By: Claude Sonnet 4.6 --- .../_templates/terraform-delta/ecommerce.tf | 5 -- .../_templates/terraform-delta/teams.tf | 56 ++++++++----------- .../_templates/terraform/scorecards.tf | 8 +-- .../terraform/_templates/terraform/teams.tf | 49 +++++++--------- 4 files changed, 49 insertions(+), 69 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf index 8c30426d..867c608d 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf @@ -41,11 +41,6 @@ resource "cortex_catalog_entity" "phoenix" { name = "Runbook" type = "runbook" url = "https://wiki.parts-unlimited.com/runbooks/phoenix" - }, - { - name = "Architecture Docs" - type = "documentation" - url = "https://wiki.parts-unlimited.com/architecture/phoenix" } ] diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf index 7c5bfd2d..82904979 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf @@ -14,28 +14,24 @@ resource "cortex_catalog_entity" "team_development" { team = { members = [ { - name = "Bill Palmer" - email = "bill.palmer@parts-unlimited.com" - role = "Team Lead" - description = "IT Manager leading the Phoenix Project" + name = "Bill Palmer" + email = "bill.palmer@parts-unlimited.com" + role = "Team Lead" }, { - name = "Maxine Chambers" - email = "maxine.chambers@parts-unlimited.com" - role = "Senior Engineer" - description = "Staff engineer on the Phoenix Project" + name = "Maxine Chambers" + email = "maxine.chambers@parts-unlimited.com" + role = "Senior Engineer" }, { - name = "Dev Magee" - email = "dev.magee@parts-unlimited.com" - role = "Engineer" - description = "Developer on the Phoenix Project" + name = "Dev Magee" + email = "dev.magee@parts-unlimited.com" + role = "Engineer" }, { - name = "Sarah Connor" - email = "sarah.connor@parts-unlimited.com" - role = "Engineer" - description = "New hire joining the Phoenix Project team" + name = "Sarah Connor" + email = "sarah.connor@parts-unlimited.com" + role = "Engineer" } ] } @@ -52,16 +48,14 @@ resource "cortex_catalog_entity" "team_operations" { team = { members = [ { - name = "Brent Geller" - email = "brent.geller@parts-unlimited.com" - role = "Principal Engineer" - description = "Indispensable operations expert and bottleneck" + name = "Brent Geller" + email = "brent.geller@parts-unlimited.com" + role = "Principal Engineer" }, { - name = "Wes Davis" - email = "wes.davis@parts-unlimited.com" - role = "Operations Manager" - description = "Manages day-to-day operations work" + name = "Wes Davis" + email = "wes.davis@parts-unlimited.com" + role = "Operations Manager" } ] } @@ -78,10 +72,9 @@ resource "cortex_catalog_entity" "team_security" { team = { members = [ { - name = "John Pesche" - email = "john.pesche@parts-unlimited.com" - role = "CISO" - description = "Chief Information Security Officer" + name = "John Pesche" + email = "john.pesche@parts-unlimited.com" + role = "CISO" } ] } @@ -98,10 +91,9 @@ resource "cortex_catalog_entity" "team_qa" { team = { members = [ { - name = "Patty McKee" - email = "patty.mckee@parts-unlimited.com" - role = "QA Manager" - description = "Manages QA processes and testing" + name = "Patty McKee" + email = "patty.mckee@parts-unlimited.com" + role = "QA Manager" } ] } diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index cbd5d7ee..0750a446 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -42,7 +42,7 @@ resource "cortex_scorecard" "production_readiness" { { title = "Has owner team" description = "Service must be owned by at least one team." - expression = "owners.teams.size() > 0" + expression = "owners.size() > 0" weight = 1 level = "Bronze" }, @@ -86,9 +86,9 @@ resource "cortex_scorecard" "production_readiness" { level = "Gold" }, { - title = "Shared ownership" - description = "Critical services should be owned by at least two teams to avoid single points of knowledge." - expression = "owners.teams.size() >= 2" + title = "Has multiple links" + description = "Service should have at least two links for comprehensive documentation and runbook coverage." + expression = "links.size() >= 2" weight = 1 level = "Gold" }, diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf index e42fc812..706baa8b 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf @@ -12,22 +12,19 @@ resource "cortex_catalog_entity" "team_development" { team = { members = [ { - name = "Bill Palmer" - email = "bill.palmer@parts-unlimited.com" - role = "Team Lead" - description = "IT Manager leading the Phoenix Project" + name = "Bill Palmer" + email = "bill.palmer@parts-unlimited.com" + role = "Team Lead" }, { - name = "Maxine Chambers" - email = "maxine.chambers@parts-unlimited.com" - role = "Senior Engineer" - description = "Staff engineer on the Phoenix Project" + name = "Maxine Chambers" + email = "maxine.chambers@parts-unlimited.com" + role = "Senior Engineer" }, { - name = "Dev Magee" - email = "dev.magee@parts-unlimited.com" - role = "Engineer" - description = "Developer on the Phoenix Project" + name = "Dev Magee" + email = "dev.magee@parts-unlimited.com" + role = "Engineer" } ] } @@ -44,16 +41,14 @@ resource "cortex_catalog_entity" "team_operations" { team = { members = [ { - name = "Brent Geller" - email = "brent.geller@parts-unlimited.com" - role = "Principal Engineer" - description = "Indispensable operations expert and bottleneck" + name = "Brent Geller" + email = "brent.geller@parts-unlimited.com" + role = "Principal Engineer" }, { - name = "Wes Davis" - email = "wes.davis@parts-unlimited.com" - role = "Operations Manager" - description = "Manages day-to-day operations work" + name = "Wes Davis" + email = "wes.davis@parts-unlimited.com" + role = "Operations Manager" } ] } @@ -70,10 +65,9 @@ resource "cortex_catalog_entity" "team_security" { team = { members = [ { - name = "John Pesche" - email = "john.pesche@parts-unlimited.com" - role = "CISO" - description = "Chief Information Security Officer" + name = "John Pesche" + email = "john.pesche@parts-unlimited.com" + role = "CISO" } ] } @@ -90,10 +84,9 @@ resource "cortex_catalog_entity" "team_qa" { team = { members = [ { - name = "Patty McKee" - email = "patty.mckee@parts-unlimited.com" - role = "QA Manager" - description = "Manages QA processes and testing" + name = "Patty McKee" + email = "patty.mckee@parts-unlimited.com" + role = "QA Manager" } ] } From 868f2f4697073960e6a34e221fb458e8c5a0c845 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 14:48:58 -0700 Subject: [PATCH 14/40] fix: use ownership.teams().length expressions in scorecard (CX-43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bronze: owners.size() > 0 → ownership.teams().length > 0 - Gold: links.size() >= 2 → ownership.teams().length >= 2 (shared ownership restored) - Delta phoenix: restore Architecture Docs link (Gold is now ownership-gated, not link-gated) Co-Authored-By: Claude Sonnet 4.6 --- .../terraform/_templates/terraform-delta/ecommerce.tf | 8 +++++++- .../terraform/_templates/terraform/scorecards.tf | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf index 867c608d..b8a6da05 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf @@ -13,7 +13,8 @@ resource "cortex_catalog_entity" "domain_ecommerce" { } # CHANGED: description expanded (≥30 chars for Silver rule 3), -# links added (Silver rule 1), metadata added (Silver rule 2) +# links added (Silver rule 1 + Gold runbook rule), metadata added (Silver rule 2) +# NOTE: Phoenix reaches Silver only — Gold requires ownership.teams().length >= 2 (shared ownership) resource "cortex_catalog_entity" "phoenix" { tag = "phoenix" name = "The Phoenix Project" @@ -41,6 +42,11 @@ resource "cortex_catalog_entity" "phoenix" { name = "Runbook" type = "runbook" url = "https://wiki.parts-unlimited.com/runbooks/phoenix" + }, + { + name = "Architecture Docs" + type = "documentation" + url = "https://wiki.parts-unlimited.com/architecture/phoenix" } ] diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index 0750a446..51ca42cc 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -42,7 +42,7 @@ resource "cortex_scorecard" "production_readiness" { { title = "Has owner team" description = "Service must be owned by at least one team." - expression = "owners.size() > 0" + expression = "ownership.teams().length > 0" weight = 1 level = "Bronze" }, @@ -86,9 +86,9 @@ resource "cortex_scorecard" "production_readiness" { level = "Gold" }, { - title = "Has multiple links" - description = "Service should have at least two links for comprehensive documentation and runbook coverage." - expression = "links.size() >= 2" + title = "Shared ownership" + description = "Service should be owned by at least two teams for bus-factor resilience." + expression = "ownership.teams().length >= 2" weight = 1 level = "Gold" }, From 7acff09c885a5b9441b67bb97228df37b9d5fcbf Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 15:14:15 -0700 Subject: [PATCH 15/40] fix: always copy template files so re-runs pick up updates (CX-43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing the already_done guard from _copy_files — file copying is idempotent and guarding it meant re-running post-install never propagated updated templates to the working directory. Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/solutions/terraform/setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/setup.py b/cortexapps_cli/solutions/terraform/setup.py index 2afe4e40..bb2040d3 100644 --- a/cortexapps_cli/solutions/terraform/setup.py +++ b/cortexapps_cli/solutions/terraform/setup.py @@ -126,13 +126,10 @@ def _create_work_dir(self) -> None: self.mark_done("create_work_dir") def _copy_files(self) -> None: - if self.already_done("copy_files"): - return work_dir = Path(self._answers["work_dir"]).expanduser() for src in _TEMPLATES_DIR.iterdir(): if src.is_file(): shutil.copy2(src, work_dir / src.name) - self.mark_done("copy_files") def _write_tfvars(self) -> None: if self.already_done("write_tfvars"): From dee1eb8c3ffdfde40b054efe6188e1dbc56ccbcf Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 15:21:42 -0700 Subject: [PATCH 16/40] fix: remove type=\"team\" from team entities, fix custom data expression (CX-43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - type=\"team\" is not a valid Cortex API type value — teams are identified by the presence of the team block, not the type field. Sending type=team in the create POST causes 400 from the API. - customData.exists() is not valid syntax; correct form is custom("key") != null Co-Authored-By: Claude Sonnet 4.6 --- .../solutions/terraform/_templates/terraform-delta/teams.tf | 4 ---- .../solutions/terraform/_templates/terraform/scorecards.tf | 2 +- .../solutions/terraform/_templates/terraform/teams.tf | 4 ---- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf index 82904979..58a992be 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf @@ -5,7 +5,6 @@ # CHANGED: Sarah Connor added resource "cortex_catalog_entity" "team_development" { tag = "team-development" - type = "team" name = "Development" description = "Application development team responsible for Parts Unlimited's e-commerce platform and core services." @@ -39,7 +38,6 @@ resource "cortex_catalog_entity" "team_development" { resource "cortex_catalog_entity" "team_operations" { tag = "team-operations" - type = "team" name = "IT Operations" description = "Infrastructure, reliability, and operations for Parts Unlimited's production systems." @@ -63,7 +61,6 @@ resource "cortex_catalog_entity" "team_operations" { resource "cortex_catalog_entity" "team_security" { tag = "team-security" - type = "team" name = "Information Security" description = "Security, compliance, and risk management for Parts Unlimited." @@ -82,7 +79,6 @@ resource "cortex_catalog_entity" "team_security" { resource "cortex_catalog_entity" "team_qa" { tag = "team-qa" - type = "team" name = "Quality Assurance" description = "Testing, QA, and release verification for Parts Unlimited services." diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index 51ca42cc..ec414c96 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -65,7 +65,7 @@ resource "cortex_scorecard" "production_readiness" { { title = "Has terraform-workspace metadata" description = "Service must declare its Terraform workspace via the terraform-workspace metadata key." - expression = "customData.exists(d, d.key == \"terraform-workspace\")" + expression = "custom(\"terraform-workspace\") != null" weight = 1 level = "Silver" }, diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf index 706baa8b..923792e7 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf @@ -3,7 +3,6 @@ resource "cortex_catalog_entity" "team_development" { tag = "team-development" - type = "team" name = "Development" description = "Application development team responsible for Parts Unlimited's e-commerce platform and core services." @@ -32,7 +31,6 @@ resource "cortex_catalog_entity" "team_development" { resource "cortex_catalog_entity" "team_operations" { tag = "team-operations" - type = "team" name = "IT Operations" description = "Infrastructure, reliability, and operations for Parts Unlimited's production systems." @@ -56,7 +54,6 @@ resource "cortex_catalog_entity" "team_operations" { resource "cortex_catalog_entity" "team_security" { tag = "team-security" - type = "team" name = "Information Security" description = "Security, compliance, and risk management for Parts Unlimited." @@ -75,7 +72,6 @@ resource "cortex_catalog_entity" "team_security" { resource "cortex_catalog_entity" "team_qa" { tag = "team-qa" - type = "team" name = "Quality Assurance" description = "Testing, QA, and release verification for Parts Unlimited services." From 571df30731e08545a3cc655ea6e7f27dd124883a Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 15:25:04 -0700 Subject: [PATCH 17/40] fix: replace links.exists() with links.size() >= 2 for Gold rule (CX-43) Cortex expression language does not support lambda-style exists() on links. Replace with a size comparison which is confirmed valid syntax. Co-Authored-By: Claude Sonnet 4.6 --- .../solutions/terraform/_templates/terraform/scorecards.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index ec414c96..a208a10f 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -93,9 +93,9 @@ resource "cortex_scorecard" "production_readiness" { level = "Gold" }, { - title = "Has runbook" - description = "Service must have a runbook link for on-call responders." - expression = "links.exists(l, l.type == \"runbook\")" + title = "Has multiple links" + description = "Service should have at least two links for comprehensive documentation coverage." + expression = "links.size() >= 2" weight = 1 level = "Gold" } From f91c42e8ccfec75d337caa937c201813cdfcece4 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 15:25:37 -0700 Subject: [PATCH 18/40] fix: use links("runbook").length > 0 for Gold runbook rule (CX-43) Correct Cortex expression syntax for filtering links by type is a function call links("type"), not a lambda predicate. Co-Authored-By: Claude Sonnet 4.6 --- .../solutions/terraform/_templates/terraform/scorecards.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index a208a10f..a2cbaeb3 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -93,9 +93,9 @@ resource "cortex_scorecard" "production_readiness" { level = "Gold" }, { - title = "Has multiple links" - description = "Service should have at least two links for comprehensive documentation coverage." - expression = "links.size() >= 2" + title = "Has runbook" + description = "Service must have a runbook link for on-call responders." + expression = "links(\"runbook\").length > 0" weight = 1 level = "Gold" } From afab489d5c4386ccd538f3d0c9fd650689c5a25f Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 15:29:43 -0700 Subject: [PATCH 19/40] fix: links.size() > 0 -> links().length > 0 for Silver rule (CX-43) Cortex expression language uses links().length (function call) not links.size() (method call). Consistent with links("type").length pattern. Co-Authored-By: Claude Sonnet 4.6 --- .../solutions/terraform/_templates/terraform/scorecards.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index a2cbaeb3..71ec7a9c 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -58,7 +58,7 @@ resource "cortex_scorecard" "production_readiness" { { title = "Has at least one link" description = "Service must have at least one link (runbook, docs, dashboard, etc.)." - expression = "links.size() > 0" + expression = "links().length > 0" weight = 1 level = "Silver" }, From 2d9fb68afa9df503ffc13ff850c0331f67b9d1fb Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 15:31:29 -0700 Subject: [PATCH 20/40] fix: try links.length > 0 for Silver has-any-link rule (CX-43) links() without args is invalid; links.length (raw list) may be the correct form for checking any link exists. Co-Authored-By: Claude Sonnet 4.6 --- .../solutions/terraform/_templates/terraform/scorecards.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index 71ec7a9c..2f2d9073 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -58,7 +58,7 @@ resource "cortex_scorecard" "production_readiness" { { title = "Has at least one link" description = "Service must have at least one link (runbook, docs, dashboard, etc.)." - expression = "links().length > 0" + expression = "links.length > 0" weight = 1 level = "Silver" }, From cb2427f28364b7cea0d465b6b8bb41e9ad652b34 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 15:33:02 -0700 Subject: [PATCH 21/40] fix: use named link types in all scorecard link rules (CX-43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit links() requires a type argument. Silver uses links("runbook").length > 0, Gold uses links("documentation").length > 0 — both types added to phoenix in the delta, keeping Bronze->Silver promotion intact. Co-Authored-By: Claude Sonnet 4.6 --- .../terraform/_templates/terraform/scorecards.tf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index 2f2d9073..06ee8c4f 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -56,9 +56,9 @@ resource "cortex_scorecard" "production_readiness" { # ── Silver ──────────────────────────────────────────────────────────────── { - title = "Has at least one link" - description = "Service must have at least one link (runbook, docs, dashboard, etc.)." - expression = "links.length > 0" + title = "Has runbook" + description = "Service must have a runbook link for operational readiness." + expression = "links(\"runbook\").length > 0" weight = 1 level = "Silver" }, @@ -93,9 +93,9 @@ resource "cortex_scorecard" "production_readiness" { level = "Gold" }, { - title = "Has runbook" - description = "Service must have a runbook link for on-call responders." - expression = "links(\"runbook\").length > 0" + title = "Has documentation" + description = "Service must have a documentation link in addition to a runbook." + expression = "links(\"documentation\").length > 0" weight = 1 level = "Gold" } From 221f7ade5ea5c6080ed206be0a46e913c92c1f8b Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 15:37:35 -0700 Subject: [PATCH 22/40] fix: define ladder levels in ascending rank order (CX-43) The Cortex API returns levels sorted Bronze->Silver->Gold (rank 1,2,3). Provider compares by position, so defining them descending caused a "inconsistent result after apply" error. Reorder to match API response. Co-Authored-By: Claude Sonnet 4.6 --- .../terraform/_templates/terraform/scorecards.tf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index 06ee8c4f..eef6b302 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -13,9 +13,9 @@ resource "cortex_scorecard" "production_readiness" { ladder = { levels = [ { - name = "Gold" - rank = 3 - color = "#D7AC58" + name = "Bronze" + rank = 1 + color = "#CD7F32" }, { name = "Silver" @@ -23,9 +23,9 @@ resource "cortex_scorecard" "production_readiness" { color = "#C0C0C0" }, { - name = "Bronze" - rank = 1 - color = "#CD7F32" + name = "Gold" + rank = 3 + color = "#D7AC58" } ] } From 40080e5b35fc80e0b6b4494acfc4cb7c70bc7dc0 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 15:50:47 -0700 Subject: [PATCH 23/40] fix: rename scorecard to avoid collision with customer scorecards (CX-43) Tag: production-readiness -> terraform-demo-production-readiness Name: Production Readiness -> Terraform Demo Production Readiness Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/solutions/terraform/README.md | 8 ++++---- .../terraform/_templates/terraform/scorecards.tf | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/README.md b/cortexapps_cli/solutions/terraform/README.md index 68392092..42404fa8 100644 --- a/cortexapps_cli/solutions/terraform/README.md +++ b/cortexapps_cli/solutions/terraform/README.md @@ -63,7 +63,7 @@ This solution installs a demo org for **Parts Unlimited** (from *The Phoenix Pro - **4 teams**: Development (Bill's team), IT Operations (Brent's domain), Information Security (John's team), QA - **2 domains**: E-Commerce, Supply Chain - **6 services**: The Phoenix Project, Parts Catalog API, Payments Service, Inventory Service, Ordering Service, Shipping Service -- **1 scorecard**: Production Readiness (Bronze/Silver/Gold) +- **1 scorecard**: Terraform Demo Production Readiness (Bronze/Silver/Gold) All services start at **Bronze** — intentionally incomplete so you can see the delta in action. @@ -87,7 +87,7 @@ You'll be asked where to create the working directory (default: `~/parts-unlimit ## Explore what was created -After install, browse your Cortex catalog filtered by the `terraform-demo` group to see all created entities. Open the **Production Readiness** scorecard to see all 6 services at Bronze. +After install, browse your Cortex catalog filtered by the `terraform-demo` group to see all created entities. Open the **Terraform Demo Production Readiness** scorecard to see all 6 services at Bronze. ## Try the Delta @@ -117,7 +117,7 @@ terraform apply **Step 4: Check the scorecard** -Open Production Readiness in Cortex. The Phoenix Project should now show **Silver**. Also check that the new **Notification Service** appears in your catalog. +Open Terraform Demo Production Readiness in Cortex. The Phoenix Project should now show **Silver**. Also check that the new **Notification Service** appears in your catalog. ## File Walkthrough @@ -131,7 +131,7 @@ Open Production Readiness in Cortex. The Phoenix Project should now show **Silve **`supply-chain.tf`** — Same pattern, owned by the supply chain team. -**`scorecards.tf`** — Owned by the platform team. Defines the Production Readiness scorecard and its Bronze/Silver/Gold rules. +**`scorecards.tf`** — Owned by the platform team. Defines the Terraform Demo Production Readiness scorecard and its Bronze/Silver/Gold rules. ## Customizing for Your Org diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index eef6b302..40ede807 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -1,12 +1,12 @@ # scorecards.tf — Platform-owned -# Defines the Production Readiness scorecard. +# Defines the Terraform Demo Production Readiness scorecard. # Bronze: automatically achieved by all properly-defined services. # Silver: requires adding links and metadata — see the delta. # Gold: requires shared ownership and a rich description — aspirational. resource "cortex_scorecard" "production_readiness" { - tag = "production-readiness" - name = "Production Readiness" + tag = "terraform-demo-production-readiness" + name = "Terraform Demo Production Readiness" description = "Measures how production-ready a Parts Unlimited service is. Bronze is table stakes; Gold is the aspirational standard." draft = false From d9d073bfb6d8ae7236628b437e37f574008af961 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 15:53:27 -0700 Subject: [PATCH 24/40] fix: scope scorecard to terraform-demo group to avoid evaluating all services (CX-43) Co-Authored-By: Claude Sonnet 4.6 --- .../solutions/terraform/_templates/terraform/scorecards.tf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index 40ede807..172b730f 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -105,6 +105,9 @@ resource "cortex_scorecard" "production_readiness" { types = { include = ["service"] } + groups = { + include = ["terraform-demo"] + } } evaluation = { From d31270c68a94a559370d351f64f42e963328362e Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 15:55:56 -0700 Subject: [PATCH 25/40] fix: add After Installing section to README for post-install menu (CX-43) The solutions framework reads ## After Installing to populate the "Next steps" option in the post-install What next? menu. Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/solutions/terraform/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/README.md b/cortexapps_cli/solutions/terraform/README.md index 42404fa8..b4266c66 100644 --- a/cortexapps_cli/solutions/terraform/README.md +++ b/cortexapps_cli/solutions/terraform/README.md @@ -85,11 +85,11 @@ You'll be asked where to create the working directory (default: `~/parts-unlimit 4. Run `terraform init` to download the Cortex provider 5. Run `terraform apply` to create all entities in Cortex -## Explore what was created +## After Installing -After install, browse your Cortex catalog filtered by the `terraform-demo` group to see all created entities. Open the **Terraform Demo Production Readiness** scorecard to see all 6 services at Bronze. +Terraform created 13 entities in your Cortex catalog: 4 teams, 2 domains, and 7 services — all tagged with the `terraform-demo` group. Filter your catalog by that group to see them, or open the **Terraform Demo Production Readiness** scorecard to see all 6 services at Bronze. -## Try the Delta +### Try the Delta The delta shows what a real team PR looks like — modify a file, plan, apply, watch the scorecard update. From b316457b9781f746a4c3a8b6ad23630035eec2e3 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 16:00:29 -0700 Subject: [PATCH 26/40] fix: add Data Model ASCII diagram to README for post-install menu (CX-43) Replaces HCL snippet as the first code block so _show_diagram() renders the flow from Terraform repo -> terraform apply -> Cortex entities. Entity tags appear verbatim for terminal hyperlink support. Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/solutions/terraform/README.md | 42 ++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/cortexapps_cli/solutions/terraform/README.md b/cortexapps_cli/solutions/terraform/README.md index b4266c66..516ac5e1 100644 --- a/cortexapps_cli/solutions/terraform/README.md +++ b/cortexapps_cli/solutions/terraform/README.md @@ -7,6 +7,48 @@ description: Manage your Cortex catalog as code using the Cortex Terraform provi The [Cortex Terraform provider](https://github.com/cortexapps/terraform-provider-cortex) lets you define your entire service catalog — teams, services, domains, scorecards — as HCL code in `.tf` files. Changes go through PR review and apply automatically on merge, giving you a fully auditable, GitOps-driven catalog. +## Data Model + +``` + ┌─────────────────────────────────────────────────┐ + │ Terraform Repo │ + │ │ + │ teams.tf scorecards.tf │ + │ ecommerce.tf provider.tf │ + │ supply-chain.tf variables.tf │ + └──────────────────────┬──────────────────────────┘ + │ + │ terraform apply + │ + ▼ + ┌─────────────────────────────────────────────────┐ + │ Cortex Catalog │ + │ │ + │ Teams │ + │ ┌───────────────────────────────────────────┐ │ + │ │ team-development team-security │ │ + │ │ team-operations team-qa │ │ + │ └───────────────────────────────────────────┘ │ + │ │ + │ Domains │ + │ ┌───────────────────────────────────────────┐ │ + │ │ domain-ecommerce domain-supply-chain │ │ + │ └───────────────────────────────────────────┘ │ + │ │ + │ Services │ + │ ┌───────────────────────────────────────────┐ │ + │ │ phoenix inventory-service │ │ + │ │ parts-catalog-api ordering-service │ │ + │ │ payments-service shipping-service │ │ + │ └───────────────────────────────────────────┘ │ + │ │ + │ Scorecard │ + │ ┌───────────────────────────────────────────┐ │ + │ │ terraform-demo-production-readiness │ │ + │ └───────────────────────────────────────────┘ │ + └─────────────────────────────────────────────────┘ +``` + ## What is HCL? HCL (HashiCorp Configuration Language) is the declarative language used in `.tf` files. It reads like structured config rather than code: From 6486e40aff484290ad19f57aeeb8d97516c8f094 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 16:01:00 -0700 Subject: [PATCH 27/40] fix: hide Import report menu option when import report is empty (CX-43) Terraform solution creates entities via terraform apply, not YAML import, so the import report is always empty. Only show the option when there's something to report. Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/commands/solutions.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cortexapps_cli/commands/solutions.py b/cortexapps_cli/commands/solutions.py index 1206951b..9d580b83 100644 --- a/cortexapps_cli/commands/solutions.py +++ b/cortexapps_cli/commands/solutions.py @@ -616,14 +616,16 @@ def _post_install_menu( options = [ ("1", "Data Model"), ("2", "Next steps"), - ("3", "Import report"), - ("4", "Exit"), ] actions = { "1": lambda: _show_diagram(readme, entity_tags=entity_tags, ui_url=ui_url), "2": lambda: _show_next_steps(readme), - "3": lambda: (console.print(), typer.echo(import_report)), } + if import_report: + options.append(("3", "Import report")) + actions["3"] = lambda: (console.print(), typer.echo(import_report)) + exit_key = str(len(options) + 1) + options.append((exit_key, "Exit")) while True: console.print() @@ -634,7 +636,7 @@ def _post_install_menu( choice = Prompt.ask("Choice", choices=[k for k, _ in options], show_choices=False) - if choice == "4": + if choice == exit_key: break actions[choice]() From 7f5fb9b0516645e90d296b99e0abbbc2f15838ba Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 16:09:21 -0700 Subject: [PATCH 28/40] feat: hyperlink .tf filenames in Data Model diagram to GitHub blob URLs (CX-43) _apply_file_hyperlinks() wraps recognized template filenames in OSC 8 terminal links pointing to the file's location in the cortexapps/cli GitHub repo. Threaded through _post_install_menu -> _show_diagram via solution_tag parameter. Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/commands/solutions.py | 49 +++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/cortexapps_cli/commands/solutions.py b/cortexapps_cli/commands/solutions.py index 9d580b83..081ad6ed 100644 --- a/cortexapps_cli/commands/solutions.py +++ b/cortexapps_cli/commands/solutions.py @@ -568,7 +568,42 @@ def _apply_hyperlinks(line: str, entity_tags: set[str], ui_url: str) -> str: return "".join(parts) -def _show_diagram(readme: str, entity_tags: set[str] | None = None, ui_url: str = "https://app.getcortexapp.com") -> None: +_GITHUB_BLOB = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions" + + +def _apply_file_hyperlinks(line: str, solution_tag: str) -> str: + """Replace bare filenames in the diagram with OSC 8 links to GitHub blob URLs.""" + base = f"{_GITHUB_BLOB}/{solution_tag}/_templates/{solution_tag}" + parts = [] + i = 0 + while i < len(line): + # Look for a word boundary start: not alphanumeric/hyphen/dot before current pos + for filename in sorted( + (f for f in [ + "teams.tf", "ecommerce.tf", "supply-chain.tf", "scorecards.tf", + "provider.tf", "variables.tf", "terraform.tfvars", + ] if line.find(f, i) == i), + key=len, reverse=True, + ): + after = i + len(filename) + # Ensure word boundary after: next char must not be alphanumeric + if after < len(line) and (line[after].isalnum() or line[after] in "-_."): + continue + parts.append(_osc8(f"{base}/{filename}", filename)) + i = after + break + else: + parts.append(line[i]) + i += 1 + return "".join(parts) + + +def _show_diagram( + readme: str, + entity_tags: set[str] | None = None, + ui_url: str = "https://app.getcortexapp.com", + solution_tag: str = "", +) -> None: block = _extract_first_codeblock(readme) if not block: return @@ -577,8 +612,11 @@ def _show_diagram(readme: str, entity_tags: set[str] | None = None, ui_url: str print() for line in block.split("\n"): - if entity_tags and links_supported: - line = _apply_hyperlinks(line, entity_tags, ui_url) + if links_supported: + if entity_tags: + line = _apply_hyperlinks(line, entity_tags, ui_url) + if solution_tag: + line = _apply_file_hyperlinks(line, solution_tag) # Use print() not console.print(): Rich counts OSC 8 escape bytes as # visible characters, shifting ASCII art alignment. print(f" {line}") @@ -612,13 +650,14 @@ def _post_install_menu( import_report: str = "", entity_tags: set[str] | None = None, ui_url: str = "https://app.getcortexapp.com", + solution_tag: str = "", ) -> None: options = [ ("1", "Data Model"), ("2", "Next steps"), ] actions = { - "1": lambda: _show_diagram(readme, entity_tags=entity_tags, ui_url=ui_url), + "1": lambda: _show_diagram(readme, entity_tags=entity_tags, ui_url=ui_url, solution_tag=solution_tag), "2": lambda: _show_next_steps(readme), } if import_report: @@ -735,7 +774,7 @@ def _do_import() -> None: entity_tags = set(resources.get("catalog", [])) except Exception: pass - _post_install_menu(readme, import_report=output, entity_tags=entity_tags, ui_url=ui_url) + _post_install_menu(readme, import_report=output, entity_tags=entity_tags, ui_url=ui_url, solution_tag=solution) @app.command(name="post-install") From 10a3e73eaad693ed6a526afd294f20611314d587 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 16:11:51 -0700 Subject: [PATCH 29/40] fix: hyperlink entity tags in Data Model diagram for terraform solution (CX-43) - _extract_tf_entity_tags() scans _templates/*.tf for tag = "..." values so entity tags are linked even when no catalog/ YAML files exist - Import report hidden when total imported = 0 and total failed = 0 (was showing for "TOTAL: 0 imported, 0 failed" despite empty result) Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/commands/solutions.py | 31 +++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/cortexapps_cli/commands/solutions.py b/cortexapps_cli/commands/solutions.py index 081ad6ed..e0c1c0c4 100644 --- a/cortexapps_cli/commands/solutions.py +++ b/cortexapps_cli/commands/solutions.py @@ -571,6 +571,21 @@ def _apply_hyperlinks(line: str, entity_tags: set[str], ui_url: str) -> str: _GITHUB_BLOB = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions" +def _extract_tf_entity_tags(solution_dir: Path) -> set[str]: + """Scan *.tf files under _templates// and return all tag = "..." values.""" + tags: set[str] = set() + templates = next( + (d for d in (solution_dir / "_templates").iterdir() if d.is_dir() and not d.name.endswith("-delta")), + None, + ) if (solution_dir / "_templates").exists() else None + if templates is None: + return tags + for tf in templates.glob("*.tf"): + for m in re.finditer(r'\btag\s*=\s*"([^"]+)"', tf.read_text(encoding="utf-8")): + tags.add(m.group(1)) + return tags + + def _apply_file_hyperlinks(line: str, solution_tag: str) -> str: """Replace bare filenames in the diagram with OSC 8 links to GitHub blob URLs.""" base = f"{_GITHUB_BLOB}/{solution_tag}/_templates/{solution_tag}" @@ -772,9 +787,23 @@ def _do_import() -> None: with as_file(root / solution) as sp: resources = _collect_solution_resources(sp) entity_tags = set(resources.get("catalog", [])) + if solutions_dir: + entity_tags |= _extract_tf_entity_tags(root / solution) + else: + with as_file(root / solution) as sp: + entity_tags |= _extract_tf_entity_tags(sp) except Exception: pass - _post_install_menu(readme, import_report=output, entity_tags=entity_tags, ui_url=ui_url, solution_tag=solution) + has_import_results = bool( + total_match and (int(total_match.group(1)) > 0 or int(total_match.group(2)) > 0) + ) + _post_install_menu( + readme, + import_report=output if has_import_results else "", + entity_tags=entity_tags, + ui_url=ui_url, + solution_tag=solution, + ) @app.command(name="post-install") From 5e6a2a9ba144aca164b7b7a40300ffbf5b5a14df Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 16:20:10 -0700 Subject: [PATCH 30/40] feat: add terraform source links to all entities; add catalog creation and scorecard links to README --- cortexapps_cli/solutions/terraform/README.md | 16 ++++++++-- .../_templates/terraform-delta/ecommerce.tf | 29 +++++++++++++++++ .../_templates/terraform-delta/teams.tf | 32 +++++++++++++++++++ .../_templates/terraform/ecommerce.tf | 24 ++++++++++++++ .../_templates/terraform/supply-chain.tf | 24 ++++++++++++++ .../terraform/_templates/terraform/teams.tf | 32 +++++++++++++++++++ 6 files changed, 155 insertions(+), 2 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/README.md b/cortexapps_cli/solutions/terraform/README.md index 516ac5e1..509cf38e 100644 --- a/cortexapps_cli/solutions/terraform/README.md +++ b/cortexapps_cli/solutions/terraform/README.md @@ -129,7 +129,19 @@ You'll be asked where to create the working directory (default: `~/parts-unlimit ## After Installing -Terraform created 13 entities in your Cortex catalog: 4 teams, 2 domains, and 7 services — all tagged with the `terraform-demo` group. Filter your catalog by that group to see them, or open the **Terraform Demo Production Readiness** scorecard to see all 6 services at Bronze. +Terraform created 13 entities in your Cortex catalog: 4 teams, 2 domains, and 6 services — all tagged with the `terraform-demo` group. + +**Create a catalog to filter to the demo entities** + +1. Go to [Catalogs](https://app.getcortexapp.com/admin/catalogs) → **New Catalog** +2. Name it **Terraform Demo** +3. Set the catalog filter: + - **Entity type:** `service` + - **Advanced options → Groups → Include:** `terraform-demo` + +**Open the scorecard** + +Open [Terraform Demo Production Readiness](https://app.getcortexapp.com/admin/scorecards/terraform-demo-production-readiness) to see all 6 services at Bronze. ### Try the Delta @@ -159,7 +171,7 @@ terraform apply **Step 4: Check the scorecard** -Open Terraform Demo Production Readiness in Cortex. The Phoenix Project should now show **Silver**. Also check that the new **Notification Service** appears in your catalog. +Open [Terraform Demo Production Readiness](https://app.getcortexapp.com/admin/scorecards/terraform-demo-production-readiness) in Cortex. The Phoenix Project should now show **Silver**. Also check that the new **Notification Service** appears in your catalog. ## File Walkthrough diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf index b8a6da05..8869bc76 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf @@ -38,6 +38,11 @@ resource "cortex_catalog_entity" "phoenix" { } links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" + }, { name = "Runbook" type = "runbook" @@ -76,6 +81,14 @@ resource "cortex_catalog_entity" "parts_catalog_api" { base_path = "/" } } + + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" + } + ] } resource "cortex_catalog_entity" "payments_service" { @@ -99,6 +112,14 @@ resource "cortex_catalog_entity" "payments_service" { base_path = "/" } } + + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" + } + ] } # NEW SERVICE — will show as `+ create` in terraform plan @@ -123,4 +144,12 @@ resource "cortex_catalog_entity" "notification_service" { base_path = "/" } } + + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf" + } + ] } diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf index 58a992be..7e3ff619 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf @@ -10,6 +10,14 @@ resource "cortex_catalog_entity" "team_development" { groups = ["terraform-demo"] + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" + } + ] + team = { members = [ { @@ -43,6 +51,14 @@ resource "cortex_catalog_entity" "team_operations" { groups = ["terraform-demo"] + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" + } + ] + team = { members = [ { @@ -66,6 +82,14 @@ resource "cortex_catalog_entity" "team_security" { groups = ["terraform-demo"] + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" + } + ] + team = { members = [ { @@ -84,6 +108,14 @@ resource "cortex_catalog_entity" "team_qa" { groups = ["terraform-demo"] + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" + } + ] + team = { members = [ { diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf index b81d3972..07f31915 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf @@ -35,6 +35,14 @@ resource "cortex_catalog_entity" "phoenix" { base_path = "/" } } + + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" + } + ] } resource "cortex_catalog_entity" "parts_catalog_api" { @@ -58,6 +66,14 @@ resource "cortex_catalog_entity" "parts_catalog_api" { base_path = "/" } } + + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" + } + ] } resource "cortex_catalog_entity" "payments_service" { @@ -81,4 +97,12 @@ resource "cortex_catalog_entity" "payments_service" { base_path = "/" } } + + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" + } + ] } diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf index 964b12bb..668034c9 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf @@ -32,6 +32,14 @@ resource "cortex_catalog_entity" "inventory_service" { base_path = "/" } } + + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf" + } + ] } resource "cortex_catalog_entity" "ordering_service" { @@ -55,6 +63,14 @@ resource "cortex_catalog_entity" "ordering_service" { base_path = "/" } } + + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf" + } + ] } resource "cortex_catalog_entity" "shipping_service" { @@ -78,4 +94,12 @@ resource "cortex_catalog_entity" "shipping_service" { base_path = "/" } } + + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf" + } + ] } diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf index 923792e7..617045f5 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf @@ -8,6 +8,14 @@ resource "cortex_catalog_entity" "team_development" { groups = ["terraform-demo"] + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" + } + ] + team = { members = [ { @@ -36,6 +44,14 @@ resource "cortex_catalog_entity" "team_operations" { groups = ["terraform-demo"] + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" + } + ] + team = { members = [ { @@ -59,6 +75,14 @@ resource "cortex_catalog_entity" "team_security" { groups = ["terraform-demo"] + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" + } + ] + team = { members = [ { @@ -77,6 +101,14 @@ resource "cortex_catalog_entity" "team_qa" { groups = ["terraform-demo"] + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" + } + ] + team = { members = [ { From 9319ce6658c65089907f5b74ccfa9545440798ef Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 16:29:53 -0700 Subject: [PATCH 31/40] fix: entity page URLs from /admin/resources?tag= to /admin/service/; add cortex-app-urls skill to repo --- .claude/skills/cortex-app-urls/SKILL.md | 66 +++++++++++++++++++ cortexapps_cli/commands/solutions.py | 4 +- .../solutions/github-actions-deploy/setup.py | 6 +- .../_templates/trigger-harness-deploy.yaml | 2 +- .../solutions/harness-deploy/setup.py | 2 +- .../_templates/trigger-jenkins-deploy.yaml | 2 +- .../solutions/jenkins-deploy/setup.py | 2 +- 7 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 .claude/skills/cortex-app-urls/SKILL.md diff --git a/.claude/skills/cortex-app-urls/SKILL.md b/.claude/skills/cortex-app-urls/SKILL.md new file mode 100644 index 00000000..23f368e5 --- /dev/null +++ b/.claude/skills/cortex-app-urls/SKILL.md @@ -0,0 +1,66 @@ +--- +name: cortex-app-urls +description: Use when generating or displaying URLs to the Cortex web application — entity pages, scorecard pages, or any link to the Cortex UI. Covers the correct URL patterns for app.getcortexapp.com and how to derive the app URL from an API base URL. +--- + +# Cortex App URL Patterns + +## STOP — memorize this before writing any URL or user-facing text + +The entity page URL is `/admin/service/` — **NOT** `/admin/resources?tag=` and **NOT** `/admin/catalog/`. + +Do not guess. Use the table below. + +## Terminology + +The correct term is **entity** (or **entities**). The word **resource** / **resources** is retired and must not appear in user-facing output, CLI messages, or documentation. + +## API vs App domains + +| Purpose | Domain | +|---------|--------| +| API calls | `api.getcortexapp.com` | +| Web UI links | `app.getcortexapp.com` | + +When you have an API base URL (e.g. from `CORTEX_BASE_URL` or the CLI session), derive the app URL by replacing `api.` with `app.`: + +```python +app_url = base_url.replace("api.", "app.", 1) if "api." in base_url else base_url +``` + +## Entity page URL + +``` +https://app.getcortexapp.com/admin/service/ +``` + +Example: `https://app.getcortexapp.com/admin/service/github-actions-demo` + +**NOT** `/admin/resources?tag=` — legacy route, does not work correctly. +**NOT** `/admin/catalog/` — that path does not exist. + +## Scorecard page URL + +``` +https://app.getcortexapp.com/admin/scorecards/ +``` + +## Other common pages + +| Page | URL pattern | +|------|-------------| +| All entities | `https://app.getcortexapp.com/admin/catalog` | +| Entity | `https://app.getcortexapp.com/admin/service/` | +| Scorecard | `https://app.getcortexapp.com/admin/scorecards/` | +| Catalogs | `https://app.getcortexapp.com/admin/catalogs` | +| Initiatives | `https://app.getcortexapp.com/admin/initiatives` | + +## In code + +When generating clickable links in terminal output, use OSC 8 hyperlinks for iTerm2/compatible terminals: + +```python +def _hyperlink(url: str, text: str = None) -> str: + label = text if text is not None else url + return f"\033]8;;{url}\033\\{label}\033]8;;\033\\" +``` diff --git a/cortexapps_cli/commands/solutions.py b/cortexapps_cli/commands/solutions.py index e0c1c0c4..e768ed6f 100644 --- a/cortexapps_cli/commands/solutions.py +++ b/cortexapps_cli/commands/solutions.py @@ -562,7 +562,7 @@ def _apply_hyperlinks(line: str, entity_tags: set[str], ui_url: str) -> str: cur = 0 for start, end, tag in matches: parts.append(line[cur:start]) - parts.append(_osc8(f"{ui_url}/admin/resources?tag={tag}", tag)) + parts.append(_osc8(f"{ui_url}/admin/service/{tag}", tag)) cur = end parts.append(line[cur:]) return "".join(parts) @@ -647,7 +647,7 @@ def _show_diagram( table.add_column("Entity") table.add_column("URL") for tag in diagram_tags: - table.add_row(tag, f"{ui_url}/admin/resources?tag={tag}") + table.add_row(tag, f"{ui_url}/admin/service/{tag}") console.print(table) diff --git a/cortexapps_cli/solutions/github-actions-deploy/setup.py b/cortexapps_cli/solutions/github-actions-deploy/setup.py index 1dc0863a..dd6299c4 100644 --- a/cortexapps_cli/solutions/github-actions-deploy/setup.py +++ b/cortexapps_cli/solutions/github-actions-deploy/setup.py @@ -200,11 +200,11 @@ def post_steps(self) -> None: repo = self._answers["repo_name"] base_url = self._answers["cortex_base_url"].rstrip("/") app_url = base_url.replace("api.", "app.", 1) if "api." in base_url else base_url - cortex_url = f"{app_url}/admin/resources?tag=github-actions-demo" + cortex_url = f"{app_url}/admin/service/github-actions-demo" gh_url = f"https://github.com/{owner}/{repo}" workflow_tag = "github-actions-deploy" - entity_url = f"{app_url}/admin/resources?tag=github-actions-demo" + entity_url = f"{app_url}/admin/service/github-actions-demo" workflows_url = f"{app_url}/admin/workflows?activeTab=runs" if self._answers.get("github_integration_alias"): @@ -367,7 +367,7 @@ def _link_github_repo(self) -> list: base_url = self._answers["cortex_base_url"].rstrip("/") api_key = self._answers["cortex_api_key"] app_url = base_url.replace("api.", "app.", 1) if "api." in base_url else base_url - entity_url = f"{app_url}/admin/resources?tag=github-actions-demo" + entity_url = f"{app_url}/admin/service/github-actions-demo" yaml_content = f"""\ openapi: "3.0.0" diff --git a/cortexapps_cli/solutions/harness-deploy/_templates/trigger-harness-deploy.yaml b/cortexapps_cli/solutions/harness-deploy/_templates/trigger-harness-deploy.yaml index 56f73df4..0fb0fb53 100644 --- a/cortexapps_cli/solutions/harness-deploy/_templates/trigger-harness-deploy.yaml +++ b/cortexapps_cli/solutions/harness-deploy/_templates/trigger-harness-deploy.yaml @@ -26,7 +26,7 @@ runResponseTemplate: | **Pipeline:** [{{variables.harness-pipeline}} in {{variables.harness-org}}/{{variables.harness-project}}](https://app.harness.io/ng/account/PLACEHOLDER_HARNESS_ACCOUNT_ID/cd/orgs/{{variables.harness-org}}/projects/{{variables.harness-project}}/pipelines/{{variables.harness-pipeline}}/executions) - **Cortex Deploys:** [see deploys for {{context.entity.tag}}](https://app.getcortexapp.com/admin/resources?tag={{context.entity.tag}}) + **Cortex Deploys:** [see deploys for {{context.entity.tag}}](https://app.getcortexapp.com/admin/service/{{context.entity.tag}}) --- diff --git a/cortexapps_cli/solutions/harness-deploy/setup.py b/cortexapps_cli/solutions/harness-deploy/setup.py index f23a8f15..c44b0f77 100644 --- a/cortexapps_cli/solutions/harness-deploy/setup.py +++ b/cortexapps_cli/solutions/harness-deploy/setup.py @@ -231,7 +231,7 @@ def post_steps(self) -> None: base_url = self._answers["cortex_base_url"].rstrip("/") app_url = base_url.replace("api.", "app.", 1) if "api." in base_url else base_url entity_tag = self._answers["entity_tag"] - cortex_url = f"{app_url}/admin/resources?tag={entity_tag}" # /admin/resources is the correct URL path (entity terminology in UI) + cortex_url = f"{app_url}/admin/service/{entity_tag}" harness_pipeline_url = ( f"{self._harness_base()}/ng/account/{self._harness_account()}" diff --git a/cortexapps_cli/solutions/jenkins-deploy/_templates/trigger-jenkins-deploy.yaml b/cortexapps_cli/solutions/jenkins-deploy/_templates/trigger-jenkins-deploy.yaml index d753d6b3..7ee3c334 100644 --- a/cortexapps_cli/solutions/jenkins-deploy/_templates/trigger-jenkins-deploy.yaml +++ b/cortexapps_cli/solutions/jenkins-deploy/_templates/trigger-jenkins-deploy.yaml @@ -17,7 +17,7 @@ runResponseTemplate: | **Job:** [{{variables.jenkins-job}}](JENKINS_BASE_URL/job/{{variables.jenkins-job}}) - **Cortex Deploys:** [see deploys for {{context.entity.tag}}](https://app.getcortexapp.com/admin/resources?tag={{context.entity.tag}}) + **Cortex Deploys:** [see deploys for {{context.entity.tag}}](https://app.getcortexapp.com/admin/service/{{context.entity.tag}}) --- diff --git a/cortexapps_cli/solutions/jenkins-deploy/setup.py b/cortexapps_cli/solutions/jenkins-deploy/setup.py index 3a000644..31ce4c51 100644 --- a/cortexapps_cli/solutions/jenkins-deploy/setup.py +++ b/cortexapps_cli/solutions/jenkins-deploy/setup.py @@ -714,7 +714,7 @@ def post_steps(self) -> None: entity_tag = self._answers["entity_tag"] workflow_tag = "jenkins-trigger-deploy" - entity_url = f"{app_url}/admin/resources?tag={entity_tag}" + entity_url = f"{app_url}/admin/service/{entity_tag}" workflows_url = f"{app_url}/admin/workflows" jenkins_url = self._answers.get("jenkins_url", "") jenkins_job = self._answers.get("jenkins_job", "cortex-deploy") From dbf1986c89e2949066b36ca4a80db4a95f65f4e1 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 16:36:31 -0700 Subject: [PATCH 32/40] fix: revert entity URLs back to /admin/resources?tag=; fix deploys URL in github-actions-deploy template; update cortex-app-urls skill --- .claude/skills/cortex-app-urls/SKILL.md | 21 ++++++++++++------- cortexapps_cli/commands/solutions.py | 4 ++-- .../solutions/github-actions-deploy/setup.py | 6 +++--- .../_templates/trigger-harness-deploy.yaml | 2 +- .../solutions/harness-deploy/setup.py | 2 +- .../_templates/trigger-jenkins-deploy.yaml | 2 +- .../solutions/jenkins-deploy/setup.py | 2 +- 7 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.claude/skills/cortex-app-urls/SKILL.md b/.claude/skills/cortex-app-urls/SKILL.md index 23f368e5..237a11a1 100644 --- a/.claude/skills/cortex-app-urls/SKILL.md +++ b/.claude/skills/cortex-app-urls/SKILL.md @@ -7,13 +7,13 @@ description: Use when generating or displaying URLs to the Cortex web applicatio ## STOP — memorize this before writing any URL or user-facing text -The entity page URL is `/admin/service/` — **NOT** `/admin/resources?tag=` and **NOT** `/admin/catalog/`. +The entity page URL is `/admin/resources?tag=` — **NOT** `/admin/service/` and **NOT** `/admin/catalog/`. Do not guess. Use the table below. ## Terminology -The correct term is **entity** (or **entities**). The word **resource** / **resources** is retired and must not appear in user-facing output, CLI messages, or documentation. +The correct term is **entity** (or **entities**). The word **resource** / **resources** is retired and must not appear in user-facing output, CLI messages, or documentation. The URL path `/admin/resources` is a legacy route but is the correct one to use for entity pages. ## API vs App domains @@ -31,14 +31,22 @@ app_url = base_url.replace("api.", "app.", 1) if "api." in base_url else base_ur ## Entity page URL ``` -https://app.getcortexapp.com/admin/service/ +https://app.getcortexapp.com/admin/resources?tag= ``` -Example: `https://app.getcortexapp.com/admin/service/github-actions-demo` +Example: `https://app.getcortexapp.com/admin/resources?tag=phoenix` -**NOT** `/admin/resources?tag=` — legacy route, does not work correctly. +**NOT** `/admin/service/` — type-specific path; teams and domains return "No entity". **NOT** `/admin/catalog/` — that path does not exist. +## Deploys page URL (entity subpage — requires numeric ID, not tag) + +``` +https://app.getcortexapp.com/admin/service//deploys +``` + +Use `GET /api/v1/catalog/` → `.id` to get the numeric ID. + ## Scorecard page URL ``` @@ -49,8 +57,7 @@ https://app.getcortexapp.com/admin/scorecards/ | Page | URL pattern | |------|-------------| -| All entities | `https://app.getcortexapp.com/admin/catalog` | -| Entity | `https://app.getcortexapp.com/admin/service/` | +| Entity | `https://app.getcortexapp.com/admin/resources?tag=` | | Scorecard | `https://app.getcortexapp.com/admin/scorecards/` | | Catalogs | `https://app.getcortexapp.com/admin/catalogs` | | Initiatives | `https://app.getcortexapp.com/admin/initiatives` | diff --git a/cortexapps_cli/commands/solutions.py b/cortexapps_cli/commands/solutions.py index e768ed6f..e0c1c0c4 100644 --- a/cortexapps_cli/commands/solutions.py +++ b/cortexapps_cli/commands/solutions.py @@ -562,7 +562,7 @@ def _apply_hyperlinks(line: str, entity_tags: set[str], ui_url: str) -> str: cur = 0 for start, end, tag in matches: parts.append(line[cur:start]) - parts.append(_osc8(f"{ui_url}/admin/service/{tag}", tag)) + parts.append(_osc8(f"{ui_url}/admin/resources?tag={tag}", tag)) cur = end parts.append(line[cur:]) return "".join(parts) @@ -647,7 +647,7 @@ def _show_diagram( table.add_column("Entity") table.add_column("URL") for tag in diagram_tags: - table.add_row(tag, f"{ui_url}/admin/service/{tag}") + table.add_row(tag, f"{ui_url}/admin/resources?tag={tag}") console.print(table) diff --git a/cortexapps_cli/solutions/github-actions-deploy/setup.py b/cortexapps_cli/solutions/github-actions-deploy/setup.py index dd6299c4..1dc0863a 100644 --- a/cortexapps_cli/solutions/github-actions-deploy/setup.py +++ b/cortexapps_cli/solutions/github-actions-deploy/setup.py @@ -200,11 +200,11 @@ def post_steps(self) -> None: repo = self._answers["repo_name"] base_url = self._answers["cortex_base_url"].rstrip("/") app_url = base_url.replace("api.", "app.", 1) if "api." in base_url else base_url - cortex_url = f"{app_url}/admin/service/github-actions-demo" + cortex_url = f"{app_url}/admin/resources?tag=github-actions-demo" gh_url = f"https://github.com/{owner}/{repo}" workflow_tag = "github-actions-deploy" - entity_url = f"{app_url}/admin/service/github-actions-demo" + entity_url = f"{app_url}/admin/resources?tag=github-actions-demo" workflows_url = f"{app_url}/admin/workflows?activeTab=runs" if self._answers.get("github_integration_alias"): @@ -367,7 +367,7 @@ def _link_github_repo(self) -> list: base_url = self._answers["cortex_base_url"].rstrip("/") api_key = self._answers["cortex_api_key"] app_url = base_url.replace("api.", "app.", 1) if "api." in base_url else base_url - entity_url = f"{app_url}/admin/service/github-actions-demo" + entity_url = f"{app_url}/admin/resources?tag=github-actions-demo" yaml_content = f"""\ openapi: "3.0.0" diff --git a/cortexapps_cli/solutions/harness-deploy/_templates/trigger-harness-deploy.yaml b/cortexapps_cli/solutions/harness-deploy/_templates/trigger-harness-deploy.yaml index 0fb0fb53..56f73df4 100644 --- a/cortexapps_cli/solutions/harness-deploy/_templates/trigger-harness-deploy.yaml +++ b/cortexapps_cli/solutions/harness-deploy/_templates/trigger-harness-deploy.yaml @@ -26,7 +26,7 @@ runResponseTemplate: | **Pipeline:** [{{variables.harness-pipeline}} in {{variables.harness-org}}/{{variables.harness-project}}](https://app.harness.io/ng/account/PLACEHOLDER_HARNESS_ACCOUNT_ID/cd/orgs/{{variables.harness-org}}/projects/{{variables.harness-project}}/pipelines/{{variables.harness-pipeline}}/executions) - **Cortex Deploys:** [see deploys for {{context.entity.tag}}](https://app.getcortexapp.com/admin/service/{{context.entity.tag}}) + **Cortex Deploys:** [see deploys for {{context.entity.tag}}](https://app.getcortexapp.com/admin/resources?tag={{context.entity.tag}}) --- diff --git a/cortexapps_cli/solutions/harness-deploy/setup.py b/cortexapps_cli/solutions/harness-deploy/setup.py index c44b0f77..b110329a 100644 --- a/cortexapps_cli/solutions/harness-deploy/setup.py +++ b/cortexapps_cli/solutions/harness-deploy/setup.py @@ -231,7 +231,7 @@ def post_steps(self) -> None: base_url = self._answers["cortex_base_url"].rstrip("/") app_url = base_url.replace("api.", "app.", 1) if "api." in base_url else base_url entity_tag = self._answers["entity_tag"] - cortex_url = f"{app_url}/admin/service/{entity_tag}" + cortex_url = f"{app_url}/admin/resources?tag={entity_tag}" harness_pipeline_url = ( f"{self._harness_base()}/ng/account/{self._harness_account()}" diff --git a/cortexapps_cli/solutions/jenkins-deploy/_templates/trigger-jenkins-deploy.yaml b/cortexapps_cli/solutions/jenkins-deploy/_templates/trigger-jenkins-deploy.yaml index 7ee3c334..d753d6b3 100644 --- a/cortexapps_cli/solutions/jenkins-deploy/_templates/trigger-jenkins-deploy.yaml +++ b/cortexapps_cli/solutions/jenkins-deploy/_templates/trigger-jenkins-deploy.yaml @@ -17,7 +17,7 @@ runResponseTemplate: | **Job:** [{{variables.jenkins-job}}](JENKINS_BASE_URL/job/{{variables.jenkins-job}}) - **Cortex Deploys:** [see deploys for {{context.entity.tag}}](https://app.getcortexapp.com/admin/service/{{context.entity.tag}}) + **Cortex Deploys:** [see deploys for {{context.entity.tag}}](https://app.getcortexapp.com/admin/resources?tag={{context.entity.tag}}) --- diff --git a/cortexapps_cli/solutions/jenkins-deploy/setup.py b/cortexapps_cli/solutions/jenkins-deploy/setup.py index 31ce4c51..3a000644 100644 --- a/cortexapps_cli/solutions/jenkins-deploy/setup.py +++ b/cortexapps_cli/solutions/jenkins-deploy/setup.py @@ -714,7 +714,7 @@ def post_steps(self) -> None: entity_tag = self._answers["entity_tag"] workflow_tag = "jenkins-trigger-deploy" - entity_url = f"{app_url}/admin/service/{entity_tag}" + entity_url = f"{app_url}/admin/resources?tag={entity_tag}" workflows_url = f"{app_url}/admin/workflows" jenkins_url = self._answers.get("jenkins_url", "") jenkins_job = self._answers.get("jenkins_job", "cortex-deploy") From 9c8ceb2a38ea6ac64a1e66d42898d88e804df277 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Wed, 2 Sep 2026 16:39:40 -0700 Subject: [PATCH 33/40] fix: scorecard tags link to /admin/scorecards/; domain source links; fix scorecard vs entity tag extraction --- cortexapps_cli/commands/solutions.py | 80 ++++++++++++++++--- .../_templates/terraform-delta/ecommerce.tf | 8 ++ .../_templates/terraform/ecommerce.tf | 8 ++ .../_templates/terraform/supply-chain.tf | 8 ++ 4 files changed, 91 insertions(+), 13 deletions(-) diff --git a/cortexapps_cli/commands/solutions.py b/cortexapps_cli/commands/solutions.py index e0c1c0c4..e04b743f 100644 --- a/cortexapps_cli/commands/solutions.py +++ b/cortexapps_cli/commands/solutions.py @@ -571,19 +571,50 @@ def _apply_hyperlinks(line: str, entity_tags: set[str], ui_url: str) -> str: _GITHUB_BLOB = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions" -def _extract_tf_entity_tags(solution_dir: Path) -> set[str]: - """Scan *.tf files under _templates// and return all tag = "..." values.""" - tags: set[str] = set() +def _parse_tf_tags_by_type(solution_dir: Path) -> tuple[set[str], set[str]]: + """Scan *.tf files under _templates// and return (entity_tags, scorecard_tags).""" + entity_tags: set[str] = set() + scorecard_tags: set[str] = set() templates = next( (d for d in (solution_dir / "_templates").iterdir() if d.is_dir() and not d.name.endswith("-delta")), None, ) if (solution_dir / "_templates").exists() else None if templates is None: - return tags + return entity_tags, scorecard_tags for tf in templates.glob("*.tf"): - for m in re.finditer(r'\btag\s*=\s*"([^"]+)"', tf.read_text(encoding="utf-8")): - tags.add(m.group(1)) - return tags + current_type: str | None = None + depth = 0 + for line in tf.read_text(encoding="utf-8").split("\n"): + m = re.match(r'\s*resource\s+"(cortex_\w+)"\s+"[^"]+"\s*\{', line) + if m: + current_type = m.group(1) + depth = 1 + continue + if current_type: + depth += line.count("{") - line.count("}") + if depth <= 0: + current_type = None + depth = 0 + continue + tm = re.search(r'\btag\s*=\s*"([^"]+)"', line) + if tm: + if current_type == "cortex_catalog_entity": + entity_tags.add(tm.group(1)) + elif current_type == "cortex_scorecard": + scorecard_tags.add(tm.group(1)) + return entity_tags, scorecard_tags + + +def _extract_tf_entity_tags(solution_dir: Path) -> set[str]: + """Return only cortex_catalog_entity tags from .tf template files.""" + entity_tags, _ = _parse_tf_tags_by_type(solution_dir) + return entity_tags + + +def _extract_tf_scorecard_tags(solution_dir: Path) -> set[str]: + """Return only cortex_scorecard tags from .tf template files.""" + _, scorecard_tags = _parse_tf_tags_by_type(solution_dir) + return scorecard_tags def _apply_file_hyperlinks(line: str, solution_tag: str) -> str: @@ -613,9 +644,18 @@ def _apply_file_hyperlinks(line: str, solution_tag: str) -> str: return "".join(parts) +def _apply_scorecard_hyperlinks(line: str, scorecard_tags: set[str], ui_url: str) -> str: + """Replace scorecard tags in a line with OSC 8 links to the scorecard page.""" + for tag in sorted(scorecard_tags, key=len, reverse=True): + if tag in line: + line = line.replace(tag, _osc8(f"{ui_url}/admin/scorecards/{tag}", tag)) + return line + + def _show_diagram( readme: str, entity_tags: set[str] | None = None, + scorecard_tags: set[str] | None = None, ui_url: str = "https://app.getcortexapp.com", solution_tag: str = "", ) -> None: @@ -630,24 +670,33 @@ def _show_diagram( if links_supported: if entity_tags: line = _apply_hyperlinks(line, entity_tags, ui_url) + if scorecard_tags: + line = _apply_scorecard_hyperlinks(line, scorecard_tags, ui_url) if solution_tag: line = _apply_file_hyperlinks(line, solution_tag) # Use print() not console.print(): Rich counts OSC 8 escape bytes as # visible characters, shifting ASCII art alignment. print(f" {line}") - if entity_tags and not links_supported: - diagram_tags = sorted( - (tag for tag in entity_tags if tag in block), + all_tags_in_block = (entity_tags or set()) | (scorecard_tags or set()) + if all_tags_in_block and not links_supported: + entity_rows = sorted( + (tag for tag in (entity_tags or set()) if tag in block), + key=lambda t: t.lower(), + ) + scorecard_rows = sorted( + (tag for tag in (scorecard_tags or set()) if tag in block), key=lambda t: t.lower(), ) - if diagram_tags: + if entity_rows or scorecard_rows: print() table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2, 0, 0)) table.add_column("Entity") table.add_column("URL") - for tag in diagram_tags: + for tag in entity_rows: table.add_row(tag, f"{ui_url}/admin/resources?tag={tag}") + for tag in scorecard_rows: + table.add_row(tag, f"{ui_url}/admin/scorecards/{tag}") console.print(table) @@ -664,6 +713,7 @@ def _post_install_menu( readme: str, import_report: str = "", entity_tags: set[str] | None = None, + scorecard_tags: set[str] | None = None, ui_url: str = "https://app.getcortexapp.com", solution_tag: str = "", ) -> None: @@ -672,7 +722,7 @@ def _post_install_menu( ("2", "Next steps"), ] actions = { - "1": lambda: _show_diagram(readme, entity_tags=entity_tags, ui_url=ui_url, solution_tag=solution_tag), + "1": lambda: _show_diagram(readme, entity_tags=entity_tags, scorecard_tags=scorecard_tags, ui_url=ui_url, solution_tag=solution_tag), "2": lambda: _show_next_steps(readme), } if import_report: @@ -779,6 +829,7 @@ def _do_import() -> None: readme = _get_readme(solution, solutions_dir) if readme: entity_tags: set[str] = set() + scorecard_tags: set[str] = set() ui_url = _get_ui_url(ctx) try: if solutions_dir: @@ -789,9 +840,11 @@ def _do_import() -> None: entity_tags = set(resources.get("catalog", [])) if solutions_dir: entity_tags |= _extract_tf_entity_tags(root / solution) + scorecard_tags |= _extract_tf_scorecard_tags(root / solution) else: with as_file(root / solution) as sp: entity_tags |= _extract_tf_entity_tags(sp) + scorecard_tags |= _extract_tf_scorecard_tags(sp) except Exception: pass has_import_results = bool( @@ -801,6 +854,7 @@ def _do_import() -> None: readme, import_report=output if has_import_results else "", entity_tags=entity_tags, + scorecard_tags=scorecard_tags, ui_url=ui_url, solution_tag=solution, ) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf index 8869bc76..1eafd7bc 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf @@ -10,6 +10,14 @@ resource "cortex_catalog_entity" "domain_ecommerce" { type = "domain" groups = ["terraform-demo"] + + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" + } + ] } # CHANGED: description expanded (≥30 chars for Silver rule 3), diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf index 07f31915..74486a88 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf @@ -12,6 +12,14 @@ resource "cortex_catalog_entity" "domain_ecommerce" { type = "domain" groups = ["terraform-demo"] + + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" + } + ] } resource "cortex_catalog_entity" "phoenix" { diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf index 668034c9..e6a07600 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf @@ -9,6 +9,14 @@ resource "cortex_catalog_entity" "domain_supply_chain" { type = "domain" groups = ["terraform-demo"] + + links = [ + { + name = "Terraform Source" + type = "source" + url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf" + } + ] } resource "cortex_catalog_entity" "inventory_service" { From 5f351dd168d220b58662e84c29020e9b1d40b440 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Thu, 3 Sep 2026 14:35:09 -0700 Subject: [PATCH 34/40] fix: use numeric scorecard ID for UI hyperlinks; update URL skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add _fetch_scorecard_id_map() to look up scorecard numeric IDs via GET /api/v1/scorecards/{tag}; falls back to tag if lookup fails - Thread scorecard_id_map (tag→id) through _show_diagram and _post_install_menu, replacing scorecard_tags: set[str] - _apply_scorecard_hyperlinks now builds /admin/scorecards/ URLs - README scorecard links changed to /admin/scorecards list page (static files cannot know the numeric ID) - Update cortex-app-urls skill: entity URL confirmed as /admin/resources?tag= for all types; scorecard requires numeric ID Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/cortex-app-urls/SKILL.md | 47 +++++++++++++++---- cortexapps_cli/commands/solutions.py | 49 ++++++++++++++------ cortexapps_cli/solutions/terraform/README.md | 4 +- 3 files changed, 75 insertions(+), 25 deletions(-) diff --git a/.claude/skills/cortex-app-urls/SKILL.md b/.claude/skills/cortex-app-urls/SKILL.md index 237a11a1..d9d67c7c 100644 --- a/.claude/skills/cortex-app-urls/SKILL.md +++ b/.claude/skills/cortex-app-urls/SKILL.md @@ -7,7 +7,8 @@ description: Use when generating or displaying URLs to the Cortex web applicatio ## STOP — memorize this before writing any URL or user-facing text -The entity page URL is `/admin/resources?tag=` — **NOT** `/admin/service/` and **NOT** `/admin/catalog/`. +The entity page URL is `/admin/resources?tag=` for ALL entity types (service, domain, team, resource). +The scorecard URL requires a **numeric ID**, not a tag: `/admin/scorecards/`. Do not guess. Use the table below. @@ -28,37 +29,63 @@ When you have an API base URL (e.g. from `CORTEX_BASE_URL` or the CLI session), app_url = base_url.replace("api.", "app.", 1) if "api." in base_url else base_url ``` -## Entity page URL +## Entity page URL (ALL entity types) ``` https://app.getcortexapp.com/admin/resources?tag= ``` +This works for services, domains, teams, and resources — all use `/admin/resources?tag=`. + Example: `https://app.getcortexapp.com/admin/resources?tag=phoenix` -**NOT** `/admin/service/` — type-specific path; teams and domains return "No entity". +**NOT** `/admin/service/` — returns "No entity" for teams and domains. **NOT** `/admin/catalog/` — that path does not exist. -## Deploys page URL (entity subpage — requires numeric ID, not tag) +## Scorecard page URL (requires numeric ID, not tag) ``` -https://app.getcortexapp.com/admin/service//deploys +https://app.getcortexapp.com/admin/scorecards/ ``` -Use `GET /api/v1/catalog/` → `.id` to get the numeric ID. +The scorecard detail page uses a numeric `id`, not the tag. Look up the ID first: + +``` +GET /api/v1/scorecards/{tag} → response["id"] +``` + +Then build: `https://app.getcortexapp.com/admin/scorecards/{id}` -## Scorecard page URL +**In code — fetch and fall back gracefully:** + +```python +def _fetch_scorecard_id_map(client, scorecard_tags: set[str]) -> dict[str, str]: + """Map tag → url_id. Falls back to tag string if API lookup fails.""" + result: dict[str, str] = {} + for tag in scorecard_tags: + try: + data = client.get(f"api/v1/scorecards/{tag}") + sc_id = data.get("id") + result[tag] = str(sc_id) if sc_id is not None else tag + except Exception: + result[tag] = tag + return result +``` + +## Deploys page URL (entity subpage — requires numeric ID, not tag) ``` -https://app.getcortexapp.com/admin/scorecards/ +https://app.getcortexapp.com/admin/service//deploys ``` +Use `GET /api/v1/catalog/` → `.id` to get the numeric ID. + ## Other common pages | Page | URL pattern | |------|-------------| -| Entity | `https://app.getcortexapp.com/admin/resources?tag=` | -| Scorecard | `https://app.getcortexapp.com/admin/scorecards/` | +| Entity (all types) | `https://app.getcortexapp.com/admin/resources?tag=` | +| Scorecard | `https://app.getcortexapp.com/admin/scorecards/` | | Catalogs | `https://app.getcortexapp.com/admin/catalogs` | | Initiatives | `https://app.getcortexapp.com/admin/initiatives` | diff --git a/cortexapps_cli/commands/solutions.py b/cortexapps_cli/commands/solutions.py index e04b743f..5ba7c299 100644 --- a/cortexapps_cli/commands/solutions.py +++ b/cortexapps_cli/commands/solutions.py @@ -644,18 +644,32 @@ def _apply_file_hyperlinks(line: str, solution_tag: str) -> str: return "".join(parts) -def _apply_scorecard_hyperlinks(line: str, scorecard_tags: set[str], ui_url: str) -> str: - """Replace scorecard tags in a line with OSC 8 links to the scorecard page.""" - for tag in sorted(scorecard_tags, key=len, reverse=True): +def _fetch_scorecard_id_map(client, scorecard_tags: set[str]) -> dict[str, str]: + """Look up numeric IDs for scorecard tags. Falls back to tag if lookup fails.""" + result: dict[str, str] = {} + for tag in scorecard_tags: + try: + data = client.get(f"api/v1/scorecards/{tag}") + sc_id = data.get("id") + result[tag] = str(sc_id) if sc_id is not None else tag + except Exception: + result[tag] = tag + return result + + +def _apply_scorecard_hyperlinks(line: str, scorecard_id_map: dict[str, str], ui_url: str) -> str: + """Replace scorecard tags in a line with OSC 8 links to the scorecard page (by numeric ID).""" + for tag in sorted(scorecard_id_map, key=len, reverse=True): if tag in line: - line = line.replace(tag, _osc8(f"{ui_url}/admin/scorecards/{tag}", tag)) + url_id = scorecard_id_map[tag] + line = line.replace(tag, _osc8(f"{ui_url}/admin/scorecards/{url_id}", tag)) return line def _show_diagram( readme: str, entity_tags: set[str] | None = None, - scorecard_tags: set[str] | None = None, + scorecard_id_map: dict[str, str] | None = None, ui_url: str = "https://app.getcortexapp.com", solution_tag: str = "", ) -> None: @@ -670,22 +684,22 @@ def _show_diagram( if links_supported: if entity_tags: line = _apply_hyperlinks(line, entity_tags, ui_url) - if scorecard_tags: - line = _apply_scorecard_hyperlinks(line, scorecard_tags, ui_url) + if scorecard_id_map: + line = _apply_scorecard_hyperlinks(line, scorecard_id_map, ui_url) if solution_tag: line = _apply_file_hyperlinks(line, solution_tag) # Use print() not console.print(): Rich counts OSC 8 escape bytes as # visible characters, shifting ASCII art alignment. print(f" {line}") - all_tags_in_block = (entity_tags or set()) | (scorecard_tags or set()) + all_tags_in_block = (entity_tags or set()) | set(scorecard_id_map or {}) if all_tags_in_block and not links_supported: entity_rows = sorted( (tag for tag in (entity_tags or set()) if tag in block), key=lambda t: t.lower(), ) scorecard_rows = sorted( - (tag for tag in (scorecard_tags or set()) if tag in block), + (tag for tag in (scorecard_id_map or {}) if tag in block), key=lambda t: t.lower(), ) if entity_rows or scorecard_rows: @@ -696,7 +710,8 @@ def _show_diagram( for tag in entity_rows: table.add_row(tag, f"{ui_url}/admin/resources?tag={tag}") for tag in scorecard_rows: - table.add_row(tag, f"{ui_url}/admin/scorecards/{tag}") + url_id = (scorecard_id_map or {})[tag] + table.add_row(tag, f"{ui_url}/admin/scorecards/{url_id}") console.print(table) @@ -713,7 +728,7 @@ def _post_install_menu( readme: str, import_report: str = "", entity_tags: set[str] | None = None, - scorecard_tags: set[str] | None = None, + scorecard_id_map: dict[str, str] | None = None, ui_url: str = "https://app.getcortexapp.com", solution_tag: str = "", ) -> None: @@ -722,7 +737,7 @@ def _post_install_menu( ("2", "Next steps"), ] actions = { - "1": lambda: _show_diagram(readme, entity_tags=entity_tags, scorecard_tags=scorecard_tags, ui_url=ui_url, solution_tag=solution_tag), + "1": lambda: _show_diagram(readme, entity_tags=entity_tags, scorecard_id_map=scorecard_id_map, ui_url=ui_url, solution_tag=solution_tag), "2": lambda: _show_next_steps(readme), } if import_report: @@ -847,6 +862,14 @@ def _do_import() -> None: scorecard_tags |= _extract_tf_scorecard_tags(sp) except Exception: pass + scorecard_id_map: dict[str, str] = {} + if scorecard_tags and ctx.obj and "client" in ctx.obj: + try: + scorecard_id_map = _fetch_scorecard_id_map(ctx.obj["client"], scorecard_tags) + except Exception: + scorecard_id_map = {tag: tag for tag in scorecard_tags} + else: + scorecard_id_map = {tag: tag for tag in scorecard_tags} has_import_results = bool( total_match and (int(total_match.group(1)) > 0 or int(total_match.group(2)) > 0) ) @@ -854,7 +877,7 @@ def _do_import() -> None: readme, import_report=output if has_import_results else "", entity_tags=entity_tags, - scorecard_tags=scorecard_tags, + scorecard_id_map=scorecard_id_map, ui_url=ui_url, solution_tag=solution, ) diff --git a/cortexapps_cli/solutions/terraform/README.md b/cortexapps_cli/solutions/terraform/README.md index 509cf38e..f2f06ec6 100644 --- a/cortexapps_cli/solutions/terraform/README.md +++ b/cortexapps_cli/solutions/terraform/README.md @@ -141,7 +141,7 @@ Terraform created 13 entities in your Cortex catalog: 4 teams, 2 domains, and 6 **Open the scorecard** -Open [Terraform Demo Production Readiness](https://app.getcortexapp.com/admin/scorecards/terraform-demo-production-readiness) to see all 6 services at Bronze. +Open [Terraform Demo Production Readiness](https://app.getcortexapp.com/admin/scorecards) to see all 6 services at Bronze. ### Try the Delta @@ -171,7 +171,7 @@ terraform apply **Step 4: Check the scorecard** -Open [Terraform Demo Production Readiness](https://app.getcortexapp.com/admin/scorecards/terraform-demo-production-readiness) in Cortex. The Phoenix Project should now show **Silver**. Also check that the new **Notification Service** appears in your catalog. +Open [Terraform Demo Production Readiness](https://app.getcortexapp.com/admin/scorecards) in Cortex. The Phoenix Project should now show **Silver**. Also check that the new **Notification Service** appears in your catalog. ## File Walkthrough From cd9311fb0e54059278e9800349c4e861472f0733 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Thu, 3 Sep 2026 14:46:32 -0700 Subject: [PATCH 35/40] chore: clarify scorecard link wording in README (links to list page) --- cortexapps_cli/solutions/terraform/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/README.md b/cortexapps_cli/solutions/terraform/README.md index f2f06ec6..d78d54ef 100644 --- a/cortexapps_cli/solutions/terraform/README.md +++ b/cortexapps_cli/solutions/terraform/README.md @@ -141,7 +141,7 @@ Terraform created 13 entities in your Cortex catalog: 4 teams, 2 domains, and 6 **Open the scorecard** -Open [Terraform Demo Production Readiness](https://app.getcortexapp.com/admin/scorecards) to see all 6 services at Bronze. +Open [Scorecards](https://app.getcortexapp.com/admin/scorecards) and find **Terraform Demo Production Readiness** to see all 6 services at Bronze. ### Try the Delta @@ -171,7 +171,7 @@ terraform apply **Step 4: Check the scorecard** -Open [Terraform Demo Production Readiness](https://app.getcortexapp.com/admin/scorecards) in Cortex. The Phoenix Project should now show **Silver**. Also check that the new **Notification Service** appears in your catalog. +Open [Scorecards](https://app.getcortexapp.com/admin/scorecards) and find **Terraform Demo Production Readiness** in Cortex. The Phoenix Project should now show **Silver**. Also check that the new **Notification Service** appears in your catalog. ## File Walkthrough From b1a2233c107b7f6522f914c3debf9088a0c93c6a Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Thu, 3 Sep 2026 15:05:32 -0700 Subject: [PATCH 36/40] fix: prefix all terraform demo entity tags with terraform-demo- Avoids tag conflicts with existing demo entities (e.g. geography demo has a "phoenix" location entity). All service, domain, and team tags now follow the terraform-demo-* convention, consistent with the existing scorecard tag. Updated: all .tf templates (initial + delta), README diagram and HCL example, setup.py delta walkthrough output. Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/solutions/terraform/README.md | 90 ++++++++++--------- .../_templates/terraform-delta/ecommerce.tf | 18 ++-- .../_templates/terraform-delta/teams.tf | 8 +- .../_templates/terraform/ecommerce.tf | 14 +-- .../_templates/terraform/supply-chain.tf | 14 +-- .../terraform/_templates/terraform/teams.tf | 8 +- cortexapps_cli/solutions/terraform/setup.py | 6 +- 7 files changed, 82 insertions(+), 76 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/README.md b/cortexapps_cli/solutions/terraform/README.md index d78d54ef..0201cb15 100644 --- a/cortexapps_cli/solutions/terraform/README.md +++ b/cortexapps_cli/solutions/terraform/README.md @@ -10,43 +10,49 @@ The [Cortex Terraform provider](https://github.com/cortexapps/terraform-provider ## Data Model ``` - ┌─────────────────────────────────────────────────┐ - │ Terraform Repo │ - │ │ - │ teams.tf scorecards.tf │ - │ ecommerce.tf provider.tf │ - │ supply-chain.tf variables.tf │ - └──────────────────────┬──────────────────────────┘ - │ - │ terraform apply - │ - ▼ - ┌─────────────────────────────────────────────────┐ - │ Cortex Catalog │ - │ │ - │ Teams │ - │ ┌───────────────────────────────────────────┐ │ - │ │ team-development team-security │ │ - │ │ team-operations team-qa │ │ - │ └───────────────────────────────────────────┘ │ - │ │ - │ Domains │ - │ ┌───────────────────────────────────────────┐ │ - │ │ domain-ecommerce domain-supply-chain │ │ - │ └───────────────────────────────────────────┘ │ - │ │ - │ Services │ - │ ┌───────────────────────────────────────────┐ │ - │ │ phoenix inventory-service │ │ - │ │ parts-catalog-api ordering-service │ │ - │ │ payments-service shipping-service │ │ - │ └───────────────────────────────────────────┘ │ - │ │ - │ Scorecard │ - │ ┌───────────────────────────────────────────┐ │ - │ │ terraform-demo-production-readiness │ │ - │ └───────────────────────────────────────────┘ │ - └─────────────────────────────────────────────────┘ + ┌──────────────────────────────────────────────────────┐ + │ Terraform Repo │ + │ │ + │ teams.tf scorecards.tf │ + │ ecommerce.tf provider.tf │ + │ supply-chain.tf variables.tf │ + └─────────────────────────────┬────────────────────────┘ + │ + │ terraform apply + │ + ▼ + ┌──────────────────────────────────────────────────────┐ + │ Cortex Catalog │ + │ │ + │ Teams │ + │ ┌────────────────────────────────────────────────┐ │ + │ │ terraform-demo-team-development │ │ + │ │ terraform-demo-team-operations │ │ + │ │ terraform-demo-team-security │ │ + │ │ terraform-demo-team-qa │ │ + │ └────────────────────────────────────────────────┘ │ + │ │ + │ Domains │ + │ ┌────────────────────────────────────────────────┐ │ + │ │ terraform-demo-domain-ecommerce │ │ + │ │ terraform-demo-domain-supply-chain │ │ + │ └────────────────────────────────────────────────┘ │ + │ │ + │ Services │ + │ ┌────────────────────────────────────────────────┐ │ + │ │ terraform-demo-phoenix │ │ + │ │ terraform-demo-parts-catalog-api │ │ + │ │ terraform-demo-payments-service │ │ + │ │ terraform-demo-inventory-service │ │ + │ │ terraform-demo-ordering-service │ │ + │ │ terraform-demo-shipping-service │ │ + │ └────────────────────────────────────────────────┘ │ + │ │ + │ Scorecard │ + │ ┌────────────────────────────────────────────────┐ │ + │ │ terraform-demo-production-readiness │ │ + │ └────────────────────────────────────────────────┘ │ + └──────────────────────────────────────────────────────┘ ``` ## What is HCL? @@ -55,11 +61,11 @@ HCL (HashiCorp Configuration Language) is the declarative language used in `.tf` ```hcl resource "cortex_catalog_entity" "phoenix" { - tag = "phoenix" + tag = "terraform-demo-phoenix" name = "The Phoenix Project" description = "Main e-commerce monolith for Parts Unlimited." - owners = [{ name = "team-development", type = "group", provider = "CORTEX" }] + owners = [{ name = "terraform-demo-team-development", type = "group", provider = "CORTEX" }] git = { github = { repository = "parts-unlimited/phoenix" } @@ -159,9 +165,9 @@ terraform plan ``` You'll see: -- `~ cortex_catalog_entity.phoenix` — **update** (adds links, metadata) -- `+ cortex_catalog_entity.notification_service` — **create** (new service) -- `~ cortex_catalog_entity.team_development` — **update** (new team member) +- `~ cortex_catalog_entity.phoenix` — **update** (adds links, metadata to `terraform-demo-phoenix`) +- `+ cortex_catalog_entity.notification_service` — **create** (`terraform-demo-notification-service`) +- `~ cortex_catalog_entity.team_development` — **update** (new member on `terraform-demo-team-development`) **Step 3: Apply** diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf index 1eafd7bc..dc68fd2a 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf @@ -4,7 +4,7 @@ # notification-service added (new Bronze service) resource "cortex_catalog_entity" "domain_ecommerce" { - tag = "domain-ecommerce" + tag = "terraform-demo-domain-ecommerce" name = "E-Commerce" description = "Customer-facing e-commerce platform including product catalog, checkout, and payments." type = "domain" @@ -24,13 +24,13 @@ resource "cortex_catalog_entity" "domain_ecommerce" { # links added (Silver rule 1 + Gold runbook rule), metadata added (Silver rule 2) # NOTE: Phoenix reaches Silver only — Gold requires ownership.teams().length >= 2 (shared ownership) resource "cortex_catalog_entity" "phoenix" { - tag = "phoenix" + tag = "terraform-demo-phoenix" name = "The Phoenix Project" description = "Main e-commerce monolith for Parts Unlimited, handling product browsing, cart, and checkout flows." owners = [ { - name = "team-development" + name = "terraform-demo-team-development" type = "group" provider = "CORTEX" } @@ -69,13 +69,13 @@ resource "cortex_catalog_entity" "phoenix" { } resource "cortex_catalog_entity" "parts_catalog_api" { - tag = "parts-catalog-api" + tag = "terraform-demo-parts-catalog-api" name = "Parts Catalog API" description = "REST API for browsing the parts catalog." owners = [ { - name = "team-development" + name = "terraform-demo-team-development" type = "group" provider = "CORTEX" } @@ -100,13 +100,13 @@ resource "cortex_catalog_entity" "parts_catalog_api" { } resource "cortex_catalog_entity" "payments_service" { - tag = "payments-service" + tag = "terraform-demo-payments-service" name = "Payments Service" description = "Payment processing and refund handling." owners = [ { - name = "team-development" + name = "terraform-demo-team-development" type = "group" provider = "CORTEX" } @@ -132,13 +132,13 @@ resource "cortex_catalog_entity" "payments_service" { # NEW SERVICE — will show as `+ create` in terraform plan resource "cortex_catalog_entity" "notification_service" { - tag = "notification-service" + tag = "terraform-demo-notification-service" name = "Notification Service" description = "Handles email, SMS, and push notifications for Parts Unlimited customer events." owners = [ { - name = "team-development" + name = "terraform-demo-team-development" type = "group" provider = "CORTEX" } diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf index 7e3ff619..62e451d6 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf @@ -4,7 +4,7 @@ # CHANGED: Sarah Connor added resource "cortex_catalog_entity" "team_development" { - tag = "team-development" + tag = "terraform-demo-team-development" name = "Development" description = "Application development team responsible for Parts Unlimited's e-commerce platform and core services." @@ -45,7 +45,7 @@ resource "cortex_catalog_entity" "team_development" { } resource "cortex_catalog_entity" "team_operations" { - tag = "team-operations" + tag = "terraform-demo-team-operations" name = "IT Operations" description = "Infrastructure, reliability, and operations for Parts Unlimited's production systems." @@ -76,7 +76,7 @@ resource "cortex_catalog_entity" "team_operations" { } resource "cortex_catalog_entity" "team_security" { - tag = "team-security" + tag = "terraform-demo-team-security" name = "Information Security" description = "Security, compliance, and risk management for Parts Unlimited." @@ -102,7 +102,7 @@ resource "cortex_catalog_entity" "team_security" { } resource "cortex_catalog_entity" "team_qa" { - tag = "team-qa" + tag = "terraform-demo-team-qa" name = "Quality Assurance" description = "Testing, QA, and release verification for Parts Unlimited services." diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf index 74486a88..320c49ab 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf @@ -6,7 +6,7 @@ # See _templates/terraform-delta/ecommerce.tf for the Silver-state version. resource "cortex_catalog_entity" "domain_ecommerce" { - tag = "domain-ecommerce" + tag = "terraform-demo-domain-ecommerce" name = "E-Commerce" description = "Customer-facing e-commerce platform including product catalog, checkout, and payments." type = "domain" @@ -23,13 +23,13 @@ resource "cortex_catalog_entity" "domain_ecommerce" { } resource "cortex_catalog_entity" "phoenix" { - tag = "phoenix" + tag = "terraform-demo-phoenix" name = "The Phoenix Project" description = "Main e-commerce monolith handling browsing and checkout." owners = [ { - name = "team-development" + name = "terraform-demo-team-development" type = "group" provider = "CORTEX" } @@ -54,13 +54,13 @@ resource "cortex_catalog_entity" "phoenix" { } resource "cortex_catalog_entity" "parts_catalog_api" { - tag = "parts-catalog-api" + tag = "terraform-demo-parts-catalog-api" name = "Parts Catalog API" description = "REST API for browsing the parts catalog." owners = [ { - name = "team-development" + name = "terraform-demo-team-development" type = "group" provider = "CORTEX" } @@ -85,13 +85,13 @@ resource "cortex_catalog_entity" "parts_catalog_api" { } resource "cortex_catalog_entity" "payments_service" { - tag = "payments-service" + tag = "terraform-demo-payments-service" name = "Payments Service" description = "Payment processing and refund handling." owners = [ { - name = "team-development" + name = "terraform-demo-team-development" type = "group" provider = "CORTEX" } diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf index e6a07600..ae19945b 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf @@ -3,7 +3,7 @@ # The supply chain team submits PRs to this file to add/update services. resource "cortex_catalog_entity" "domain_supply_chain" { - tag = "domain-supply-chain" + tag = "terraform-demo-domain-supply-chain" name = "Supply Chain" description = "Inventory, ordering, and shipping services supporting Parts Unlimited's fulfillment operations." type = "domain" @@ -20,13 +20,13 @@ resource "cortex_catalog_entity" "domain_supply_chain" { } resource "cortex_catalog_entity" "inventory_service" { - tag = "inventory-service" + tag = "terraform-demo-inventory-service" name = "Inventory Service" description = "Real-time inventory tracking across all Parts Unlimited warehouses." owners = [ { - name = "team-operations" + name = "terraform-demo-team-operations" type = "group" provider = "CORTEX" } @@ -51,13 +51,13 @@ resource "cortex_catalog_entity" "inventory_service" { } resource "cortex_catalog_entity" "ordering_service" { - tag = "ordering-service" + tag = "terraform-demo-ordering-service" name = "Ordering Service" description = "Order placement, validation, and fulfillment coordination." owners = [ { - name = "team-development" + name = "terraform-demo-team-development" type = "group" provider = "CORTEX" } @@ -82,13 +82,13 @@ resource "cortex_catalog_entity" "ordering_service" { } resource "cortex_catalog_entity" "shipping_service" { - tag = "shipping-service" + tag = "terraform-demo-shipping-service" name = "Shipping Service" description = "Shipping and logistics tracking for Parts Unlimited orders." owners = [ { - name = "team-operations" + name = "terraform-demo-team-operations" type = "group" provider = "CORTEX" } diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf index 617045f5..34be1d5a 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf @@ -2,7 +2,7 @@ # Changes to teams (membership, new hires, reorgs) are made here via platform PR. resource "cortex_catalog_entity" "team_development" { - tag = "team-development" + tag = "terraform-demo-team-development" name = "Development" description = "Application development team responsible for Parts Unlimited's e-commerce platform and core services." @@ -38,7 +38,7 @@ resource "cortex_catalog_entity" "team_development" { } resource "cortex_catalog_entity" "team_operations" { - tag = "team-operations" + tag = "terraform-demo-team-operations" name = "IT Operations" description = "Infrastructure, reliability, and operations for Parts Unlimited's production systems." @@ -69,7 +69,7 @@ resource "cortex_catalog_entity" "team_operations" { } resource "cortex_catalog_entity" "team_security" { - tag = "team-security" + tag = "terraform-demo-team-security" name = "Information Security" description = "Security, compliance, and risk management for Parts Unlimited." @@ -95,7 +95,7 @@ resource "cortex_catalog_entity" "team_security" { } resource "cortex_catalog_entity" "team_qa" { - tag = "team-qa" + tag = "terraform-demo-team-qa" name = "Quality Assurance" description = "Testing, QA, and release verification for Parts Unlimited services." diff --git a/cortexapps_cli/solutions/terraform/setup.py b/cortexapps_cli/solutions/terraform/setup.py index bb2040d3..e3477ec4 100644 --- a/cortexapps_cli/solutions/terraform/setup.py +++ b/cortexapps_cli/solutions/terraform/setup.py @@ -77,9 +77,9 @@ def post_steps(self) -> None: print(" 2. Preview the changes:") print(f" cd {work_dir} && terraform plan\n") print(" Look for:") - print(" ~ cortex_catalog_entity.phoenix (update: links + metadata added)") - print(" + cortex_catalog_entity.notification_service (create: new service)") - print(" ~ cortex_catalog_entity.team_development (update: new member)\n") + print(" ~ cortex_catalog_entity.phoenix (update: terraform-demo-phoenix)") + print(" + cortex_catalog_entity.notification_service (create: terraform-demo-notification-service)") + print(" ~ cortex_catalog_entity.team_development (update: terraform-demo-team-development)\n") print(" 3. Apply:") print(f" terraform apply\n") print(" 4. Check the Production Readiness scorecard in Cortex.") From bde1fa1842633475c98d7e77a4e33fc7a016343c Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Thu, 3 Sep 2026 15:22:17 -0700 Subject: [PATCH 37/40] fix: remove spurious 404 from scorecard id lookup in terraform solution The Cortex scorecard API does not return a numeric id field; the URL format uses the tag directly. Remove the API call in _fetch_scorecard_id_map to avoid a printed 404 error that leaked through the exception handler. Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/commands/solutions.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/cortexapps_cli/commands/solutions.py b/cortexapps_cli/commands/solutions.py index 5ba7c299..30318c78 100644 --- a/cortexapps_cli/commands/solutions.py +++ b/cortexapps_cli/commands/solutions.py @@ -644,17 +644,10 @@ def _apply_file_hyperlinks(line: str, solution_tag: str) -> str: return "".join(parts) -def _fetch_scorecard_id_map(client, scorecard_tags: set[str]) -> dict[str, str]: - """Look up numeric IDs for scorecard tags. Falls back to tag if lookup fails.""" - result: dict[str, str] = {} - for tag in scorecard_tags: - try: - data = client.get(f"api/v1/scorecards/{tag}") - sc_id = data.get("id") - result[tag] = str(sc_id) if sc_id is not None else tag - except Exception: - result[tag] = tag - return result +def _fetch_scorecard_id_map(scorecard_tags: set[str]) -> dict[str, str]: + """Return a tag→tag map for scorecard URL generation. + Cortex scorecard URLs use the tag directly (not a numeric ID).""" + return {tag: tag for tag in scorecard_tags} def _apply_scorecard_hyperlinks(line: str, scorecard_id_map: dict[str, str], ui_url: str) -> str: @@ -862,14 +855,7 @@ def _do_import() -> None: scorecard_tags |= _extract_tf_scorecard_tags(sp) except Exception: pass - scorecard_id_map: dict[str, str] = {} - if scorecard_tags and ctx.obj and "client" in ctx.obj: - try: - scorecard_id_map = _fetch_scorecard_id_map(ctx.obj["client"], scorecard_tags) - except Exception: - scorecard_id_map = {tag: tag for tag in scorecard_tags} - else: - scorecard_id_map = {tag: tag for tag in scorecard_tags} + scorecard_id_map = _fetch_scorecard_id_map(scorecard_tags) has_import_results = bool( total_match and (int(total_match.group(1)) > 0 or int(total_match.group(2)) > 0) ) From 3cb313647ecdd9431bdbece21f82578ee6937653 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Thu, 3 Sep 2026 15:38:51 -0700 Subject: [PATCH 38/40] fix: remove scorecard hyperlink from data model diagram Cortex scorecard page URLs require a numeric ID that is not available from the public API. Remove the scorecard hyperlink entirely; the scorecard tag now appears as plain text in the diagram. Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/commands/solutions.py | 44 +++------------------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/cortexapps_cli/commands/solutions.py b/cortexapps_cli/commands/solutions.py index 30318c78..da8dd04b 100644 --- a/cortexapps_cli/commands/solutions.py +++ b/cortexapps_cli/commands/solutions.py @@ -611,11 +611,6 @@ def _extract_tf_entity_tags(solution_dir: Path) -> set[str]: return entity_tags -def _extract_tf_scorecard_tags(solution_dir: Path) -> set[str]: - """Return only cortex_scorecard tags from .tf template files.""" - _, scorecard_tags = _parse_tf_tags_by_type(solution_dir) - return scorecard_tags - def _apply_file_hyperlinks(line: str, solution_tag: str) -> str: """Replace bare filenames in the diagram with OSC 8 links to GitHub blob URLs.""" @@ -644,25 +639,10 @@ def _apply_file_hyperlinks(line: str, solution_tag: str) -> str: return "".join(parts) -def _fetch_scorecard_id_map(scorecard_tags: set[str]) -> dict[str, str]: - """Return a tag→tag map for scorecard URL generation. - Cortex scorecard URLs use the tag directly (not a numeric ID).""" - return {tag: tag for tag in scorecard_tags} - - -def _apply_scorecard_hyperlinks(line: str, scorecard_id_map: dict[str, str], ui_url: str) -> str: - """Replace scorecard tags in a line with OSC 8 links to the scorecard page (by numeric ID).""" - for tag in sorted(scorecard_id_map, key=len, reverse=True): - if tag in line: - url_id = scorecard_id_map[tag] - line = line.replace(tag, _osc8(f"{ui_url}/admin/scorecards/{url_id}", tag)) - return line - def _show_diagram( readme: str, entity_tags: set[str] | None = None, - scorecard_id_map: dict[str, str] | None = None, ui_url: str = "https://app.getcortexapp.com", solution_tag: str = "", ) -> None: @@ -677,34 +657,24 @@ def _show_diagram( if links_supported: if entity_tags: line = _apply_hyperlinks(line, entity_tags, ui_url) - if scorecard_id_map: - line = _apply_scorecard_hyperlinks(line, scorecard_id_map, ui_url) if solution_tag: line = _apply_file_hyperlinks(line, solution_tag) # Use print() not console.print(): Rich counts OSC 8 escape bytes as # visible characters, shifting ASCII art alignment. print(f" {line}") - all_tags_in_block = (entity_tags or set()) | set(scorecard_id_map or {}) - if all_tags_in_block and not links_supported: + if entity_tags and not links_supported: entity_rows = sorted( - (tag for tag in (entity_tags or set()) if tag in block), - key=lambda t: t.lower(), - ) - scorecard_rows = sorted( - (tag for tag in (scorecard_id_map or {}) if tag in block), + (tag for tag in entity_tags if tag in block), key=lambda t: t.lower(), ) - if entity_rows or scorecard_rows: + if entity_rows: print() table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2, 0, 0)) table.add_column("Entity") table.add_column("URL") for tag in entity_rows: table.add_row(tag, f"{ui_url}/admin/resources?tag={tag}") - for tag in scorecard_rows: - url_id = (scorecard_id_map or {})[tag] - table.add_row(tag, f"{ui_url}/admin/scorecards/{url_id}") console.print(table) @@ -721,7 +691,6 @@ def _post_install_menu( readme: str, import_report: str = "", entity_tags: set[str] | None = None, - scorecard_id_map: dict[str, str] | None = None, ui_url: str = "https://app.getcortexapp.com", solution_tag: str = "", ) -> None: @@ -730,7 +699,7 @@ def _post_install_menu( ("2", "Next steps"), ] actions = { - "1": lambda: _show_diagram(readme, entity_tags=entity_tags, scorecard_id_map=scorecard_id_map, ui_url=ui_url, solution_tag=solution_tag), + "1": lambda: _show_diagram(readme, entity_tags=entity_tags, ui_url=ui_url, solution_tag=solution_tag), "2": lambda: _show_next_steps(readme), } if import_report: @@ -837,7 +806,6 @@ def _do_import() -> None: readme = _get_readme(solution, solutions_dir) if readme: entity_tags: set[str] = set() - scorecard_tags: set[str] = set() ui_url = _get_ui_url(ctx) try: if solutions_dir: @@ -848,14 +816,11 @@ def _do_import() -> None: entity_tags = set(resources.get("catalog", [])) if solutions_dir: entity_tags |= _extract_tf_entity_tags(root / solution) - scorecard_tags |= _extract_tf_scorecard_tags(root / solution) else: with as_file(root / solution) as sp: entity_tags |= _extract_tf_entity_tags(sp) - scorecard_tags |= _extract_tf_scorecard_tags(sp) except Exception: pass - scorecard_id_map = _fetch_scorecard_id_map(scorecard_tags) has_import_results = bool( total_match and (int(total_match.group(1)) > 0 or int(total_match.group(2)) > 0) ) @@ -863,7 +828,6 @@ def _do_import() -> None: readme, import_report=output if has_import_results else "", entity_tags=entity_tags, - scorecard_id_map=scorecard_id_map, ui_url=ui_url, solution_tag=solution, ) From 8638e6fca27df4368278a3b5ee26b20e584323bd Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Thu, 3 Sep 2026 15:50:44 -0700 Subject: [PATCH 39/40] fix: correct team type, link type, role format, and scorecard CQL (CX-43) - Add type = "team" to all four team entities in teams.tf and terraform-delta/teams.tf - Change link type from "source" to "documentation" across all .tf files - Hyphenate member role values (e.g. "Team Lead" -> "Team-Lead") to satisfy Cortex API validation (roles must be letters, digits, and hyphens only) - Fix scorecard CQL ownership expressions to chain null-safe access: ownership?.teams()?.length > 0 (previously .length on null result crashed) Co-Authored-By: Claude Sonnet 4.6 --- .../_templates/terraform-delta/ecommerce.tf | 10 ++++----- .../_templates/terraform-delta/teams.tf | 20 +++++++++-------- .../_templates/terraform/ecommerce.tf | 8 +++---- .../_templates/terraform/scorecards.tf | 4 ++-- .../_templates/terraform/supply-chain.tf | 8 +++---- .../terraform/_templates/terraform/teams.tf | 22 +++++++++++-------- 6 files changed, 39 insertions(+), 33 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf index dc68fd2a..b925fc58 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf @@ -14,7 +14,7 @@ resource "cortex_catalog_entity" "domain_ecommerce" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" } ] @@ -48,7 +48,7 @@ resource "cortex_catalog_entity" "phoenix" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" }, { @@ -93,7 +93,7 @@ resource "cortex_catalog_entity" "parts_catalog_api" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" } ] @@ -124,7 +124,7 @@ resource "cortex_catalog_entity" "payments_service" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" } ] @@ -156,7 +156,7 @@ resource "cortex_catalog_entity" "notification_service" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform-delta/ecommerce.tf" } ] diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf index 62e451d6..eff304b0 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform-delta/teams.tf @@ -6,6 +6,7 @@ resource "cortex_catalog_entity" "team_development" { tag = "terraform-demo-team-development" name = "Development" + type = "team" description = "Application development team responsible for Parts Unlimited's e-commerce platform and core services." groups = ["terraform-demo"] @@ -13,7 +14,7 @@ resource "cortex_catalog_entity" "team_development" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" } ] @@ -23,12 +24,12 @@ resource "cortex_catalog_entity" "team_development" { { name = "Bill Palmer" email = "bill.palmer@parts-unlimited.com" - role = "Team Lead" + role = "Team-Lead" }, { name = "Maxine Chambers" email = "maxine.chambers@parts-unlimited.com" - role = "Senior Engineer" + role = "Senior-Engineer" }, { name = "Dev Magee" @@ -47,6 +48,7 @@ resource "cortex_catalog_entity" "team_development" { resource "cortex_catalog_entity" "team_operations" { tag = "terraform-demo-team-operations" name = "IT Operations" + type = "team" description = "Infrastructure, reliability, and operations for Parts Unlimited's production systems." groups = ["terraform-demo"] @@ -54,7 +56,7 @@ resource "cortex_catalog_entity" "team_operations" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" } ] @@ -64,12 +66,12 @@ resource "cortex_catalog_entity" "team_operations" { { name = "Brent Geller" email = "brent.geller@parts-unlimited.com" - role = "Principal Engineer" + role = "Principal-Engineer" }, { name = "Wes Davis" email = "wes.davis@parts-unlimited.com" - role = "Operations Manager" + role = "Operations-Manager" } ] } @@ -85,7 +87,7 @@ resource "cortex_catalog_entity" "team_security" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" } ] @@ -111,7 +113,7 @@ resource "cortex_catalog_entity" "team_qa" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" } ] @@ -121,7 +123,7 @@ resource "cortex_catalog_entity" "team_qa" { { name = "Patty McKee" email = "patty.mckee@parts-unlimited.com" - role = "QA Manager" + role = "QA-Manager" } ] } diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf index 320c49ab..ecfbbd47 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf @@ -16,7 +16,7 @@ resource "cortex_catalog_entity" "domain_ecommerce" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" } ] @@ -47,7 +47,7 @@ resource "cortex_catalog_entity" "phoenix" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" } ] @@ -78,7 +78,7 @@ resource "cortex_catalog_entity" "parts_catalog_api" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" } ] @@ -109,7 +109,7 @@ resource "cortex_catalog_entity" "payments_service" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/ecommerce.tf" } ] diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index 172b730f..70771c8b 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -42,7 +42,7 @@ resource "cortex_scorecard" "production_readiness" { { title = "Has owner team" description = "Service must be owned by at least one team." - expression = "ownership.teams().length > 0" + expression = "ownership?.teams()?.length > 0" weight = 1 level = "Bronze" }, @@ -88,7 +88,7 @@ resource "cortex_scorecard" "production_readiness" { { title = "Shared ownership" description = "Service should be owned by at least two teams for bus-factor resilience." - expression = "ownership.teams().length >= 2" + expression = "ownership?.teams()?.length >= 2" weight = 1 level = "Gold" }, diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf index ae19945b..cf5d12cc 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf @@ -13,7 +13,7 @@ resource "cortex_catalog_entity" "domain_supply_chain" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf" } ] @@ -44,7 +44,7 @@ resource "cortex_catalog_entity" "inventory_service" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf" } ] @@ -75,7 +75,7 @@ resource "cortex_catalog_entity" "ordering_service" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf" } ] @@ -106,7 +106,7 @@ resource "cortex_catalog_entity" "shipping_service" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/supply-chain.tf" } ] diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf index 34be1d5a..5ca58807 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf @@ -4,6 +4,7 @@ resource "cortex_catalog_entity" "team_development" { tag = "terraform-demo-team-development" name = "Development" + type = "team" description = "Application development team responsible for Parts Unlimited's e-commerce platform and core services." groups = ["terraform-demo"] @@ -11,7 +12,7 @@ resource "cortex_catalog_entity" "team_development" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" } ] @@ -21,12 +22,12 @@ resource "cortex_catalog_entity" "team_development" { { name = "Bill Palmer" email = "bill.palmer@parts-unlimited.com" - role = "Team Lead" + role = "Team-Lead" }, { name = "Maxine Chambers" email = "maxine.chambers@parts-unlimited.com" - role = "Senior Engineer" + role = "Senior-Engineer" }, { name = "Dev Magee" @@ -40,6 +41,7 @@ resource "cortex_catalog_entity" "team_development" { resource "cortex_catalog_entity" "team_operations" { tag = "terraform-demo-team-operations" name = "IT Operations" + type = "team" description = "Infrastructure, reliability, and operations for Parts Unlimited's production systems." groups = ["terraform-demo"] @@ -47,7 +49,7 @@ resource "cortex_catalog_entity" "team_operations" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" } ] @@ -57,12 +59,12 @@ resource "cortex_catalog_entity" "team_operations" { { name = "Brent Geller" email = "brent.geller@parts-unlimited.com" - role = "Principal Engineer" + role = "Principal-Engineer" }, { name = "Wes Davis" email = "wes.davis@parts-unlimited.com" - role = "Operations Manager" + role = "Operations-Manager" } ] } @@ -71,6 +73,7 @@ resource "cortex_catalog_entity" "team_operations" { resource "cortex_catalog_entity" "team_security" { tag = "terraform-demo-team-security" name = "Information Security" + type = "team" description = "Security, compliance, and risk management for Parts Unlimited." groups = ["terraform-demo"] @@ -78,7 +81,7 @@ resource "cortex_catalog_entity" "team_security" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" } ] @@ -97,6 +100,7 @@ resource "cortex_catalog_entity" "team_security" { resource "cortex_catalog_entity" "team_qa" { tag = "terraform-demo-team-qa" name = "Quality Assurance" + type = "team" description = "Testing, QA, and release verification for Parts Unlimited services." groups = ["terraform-demo"] @@ -104,7 +108,7 @@ resource "cortex_catalog_entity" "team_qa" { links = [ { name = "Terraform Source" - type = "source" + type = "documentation" url = "https://github.com/cortexapps/cli/blob/main/cortexapps_cli/solutions/terraform/_templates/terraform/teams.tf" } ] @@ -114,7 +118,7 @@ resource "cortex_catalog_entity" "team_qa" { { name = "Patty McKee" email = "patty.mckee@parts-unlimited.com" - role = "QA Manager" + role = "QA-Manager" } ] } From 002086cc49e53dcf7f61e96406c10ff3a3611986 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Thu, 3 Sep 2026 16:16:55 -0700 Subject: [PATCH 40/40] fix: revert ownership CQL to clean expression (no null-safe needed) ownership.teams().length > 0 works correctly once team entities have type = "team" set. The null access error was caused by teams not being recognized as teams, not by a syntax issue with the CQL expression. Co-Authored-By: Claude Sonnet 4.6 --- .../solutions/terraform/_templates/terraform/scorecards.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf index 70771c8b..172b730f 100644 --- a/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf +++ b/cortexapps_cli/solutions/terraform/_templates/terraform/scorecards.tf @@ -42,7 +42,7 @@ resource "cortex_scorecard" "production_readiness" { { title = "Has owner team" description = "Service must be owned by at least one team." - expression = "ownership?.teams()?.length > 0" + expression = "ownership.teams().length > 0" weight = 1 level = "Bronze" }, @@ -88,7 +88,7 @@ resource "cortex_scorecard" "production_readiness" { { title = "Shared ownership" description = "Service should be owned by at least two teams for bus-factor resilience." - expression = "ownership?.teams()?.length >= 2" + expression = "ownership.teams().length >= 2" weight = 1 level = "Gold" },