From adad148babe03a8dfac68a2c8e36784276ecb0e8 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 09:32:11 -0700 Subject: [PATCH 01/20] docs: add workday-integration solution design spec Co-Authored-By: Claude Sonnet 4.6 --- ...-14-workday-integration-solution-design.md | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-14-workday-integration-solution-design.md diff --git a/docs/superpowers/specs/2026-08-14-workday-integration-solution-design.md b/docs/superpowers/specs/2026-08-14-workday-integration-solution-design.md new file mode 100644 index 0000000..cf0af8f --- /dev/null +++ b/docs/superpowers/specs/2026-08-14-workday-integration-solution-design.md @@ -0,0 +1,130 @@ +# Workday Integration Solution Design + +> **Status:** Approved + +## Goal + +Add a `workday-integration` Cortex CLI solution that configures the Cortex Workday integration to sync a Pied Piper org hierarchy from a public JSON report bundled in the CLI repo. + +## Architecture + +A minimal solution: bundled static data + a `configuration.json` field mapping + a lightweight setup wizard that makes one Cortex API call. No external services to create, no scorecards, no entity types to install. The Workday integration in Cortex handles entity creation when the user triggers the sync. + +## Tech Stack + +- Python 3.11+, `requests`, `SolutionSetup` base class (same pattern as `github-actions-deploy`) +- Data served via `raw.githubusercontent.com` (no GitHub Pages setup required) + +## Global Constraints + +- Solution tag: `workday-integration` +- Data file: `ONE_EMPLOYEE_ONE_TEAM` hierarchy format +- Report URL: `https://raw.githubusercontent.com/cortexapps/cli/main/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json` +- No scorecards, no catalog entity imports, no entity types +- `SolutionSetup` base class must be imported dynamically (same pattern as `github-actions-deploy/setup.py`) +- State file lives at `~/.cortex/solutions/workday-integration.json` + +--- + +## File Structure + +``` +cortexapps_cli/solutions/workday-integration/ +├── README.md +├── setup.py +└── data/ + ├── pied-piper-hierarchy.json # Pied Piper org report (copied from workday-mocks) + └── configuration.json # Cortex Workday integration config (static, bundled) +``` + +--- + +## Data + +### `data/pied-piper-hierarchy.json` + +Copied verbatim from `~/git/jeff-test-org/workday-mocks/pied-piper-hierarchy/index.json`. + +`ONE_EMPLOYEE_ONE_TEAM` format. Each entry has: `email`, `employeeId`, `firstName`, `lastName`, `managersEmail`, `teamId`, `teamName`, `parentTeamId`. Root employee self-references in `managersEmail`. Root teams have `parentTeamId: "NONE"`. + +### `data/configuration.json` + +Static — the URL is fixed at the raw.githubusercontent.com path above. + +```json +{ + "username": "ISU_Cortex", + "ownershipReportUrl": "https://raw.githubusercontent.com/cortexapps/cli/main/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json", + "reportMappingV2": { + "email": { "columnName": "email" }, + "employeeId": { "columnName": "employeeId" }, + "firstName": { "columnName": "firstName" }, + "lastName": { "columnName": "lastName" }, + "managerEmail": { "columnName": "managersEmail" }, + "employeeRole": null, + "rootTeams": [], + "teamListFields": null, + "fallbackFields": { + "teamId": { "columnName": "teamId" }, + "teamName": { "columnName": "teamName" }, + "fieldOnParentNode": { "columnName": "teamId" }, + "fieldOnChildNode": { "columnName": "parentTeamId" } + }, + "type": "ONE_EMPLOYEE_ONE_TEAM" + }, + "password": "asdf" +} +``` + +--- + +## Setup Script (`setup.py`) + +Extends `SolutionSetup`. Minimal — two prompts, one API call. + +### `collect_prompts()` + +1. **Cortex API key** — defaults to `CORTEX_API_KEY` env var / session key +2. **Cortex base URL** — defaults to `CORTEX_BASE_URL` env var / session URL + +### `steps()` + +Single step: **Configure Workday integration** +- Reads `data/configuration.json` from the solution directory +- Calls `POST {cortex_base_url}/api/v1/integrations/workday` with the config as JSON body +- Auth header: `Authorization: Bearer {cortex_api_key}` +- On success: prints confirmation +- On error: raises with response body for diagnosis + +Uses `already_done` / `mark_done` for idempotency (re-running post-install skips if already configured). + +### `post_steps()` + +Prints: + +``` +✓ Workday integration configured with the Pied Piper org hierarchy. + +Next: trigger the import in Cortex: + Catalog → All Entities → Import Entities + +Then check your team hierarchy to see the Pied Piper org chart. +``` + +--- + +## README.md + +Covers: +- What the solution installs (integration config + Pied Piper data) +- Quick start (`cortex solutions install -s workday-integration`) +- How to trigger the sync (Catalog → All Entities → Import Entities) +- What to expect after sync (employees + team hierarchy in Cortex) +- How to adapt to real Workday data (swap `ownershipReportUrl`, set real `username`/`password`) + +--- + +## Testing + +- Unit tests for `setup.py`: mock the `requests.post` call, verify correct URL + payload +- Integration test: `cortex solutions post-install -s workday-integration --no-prompt` against a sandbox tenant From 6790926da6397465ca1752b419e71441c9bcf92e Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 10:26:49 -0700 Subject: [PATCH 02/20] =?UTF-8?q?docs:=20update=20workday=20solution=20spe?= =?UTF-8?q?c=20=E2=80=94=20no=20credential=20prompts,=20backup=20existing?= =?UTF-8?q?=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- ...-14-workday-integration-solution-design.md | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/superpowers/specs/2026-08-14-workday-integration-solution-design.md b/docs/superpowers/specs/2026-08-14-workday-integration-solution-design.md index cf0af8f..51247da 100644 --- a/docs/superpowers/specs/2026-08-14-workday-integration-solution-design.md +++ b/docs/superpowers/specs/2026-08-14-workday-integration-solution-design.md @@ -80,23 +80,33 @@ Static — the URL is fixed at the raw.githubusercontent.com path above. ## Setup Script (`setup.py`) -Extends `SolutionSetup`. Minimal — two prompts, one API call. +Extends `SolutionSetup`. No prompts for credentials — the CLI already has them via `ctx`. Zero prompts in `collect_prompts()`. + +Credentials are read from `ctx.obj["client"]` (the `CortexClient` already configured for the session): `client.api_key` and `client.base_url`. ### `collect_prompts()` -1. **Cortex API key** — defaults to `CORTEX_API_KEY` env var / session key -2. **Cortex base URL** — defaults to `CORTEX_BASE_URL` env var / session URL +Empty — no user prompts needed. ### `steps()` -Single step: **Configure Workday integration** +**Step 1: Check for existing Workday integration** +- `GET {base_url}/api/v1/integrations/workday` +- If 404: no existing config, proceed to Step 2 +- If 200: existing integration found — prompt: `"Existing Workday integration found. Replace it? [y/N]"` + - If N: abort with message `"Keeping existing Workday integration. Exiting."` + - If Y: + - Write existing config response body to `~/.cortex/solutions/workday-integration/backup-config.json` + - `DELETE {base_url}/api/v1/integrations/workday` + +**Step 2: Configure Workday integration** - Reads `data/configuration.json` from the solution directory -- Calls `POST {cortex_base_url}/api/v1/integrations/workday` with the config as JSON body -- Auth header: `Authorization: Bearer {cortex_api_key}` +- `POST {base_url}/api/v1/integrations/workday` with config as JSON body +- Auth header: `Authorization: Bearer {api_key}` - On success: prints confirmation - On error: raises with response body for diagnosis -Uses `already_done` / `mark_done` for idempotency (re-running post-install skips if already configured). +Uses `already_done` / `mark_done` for idempotency (re-running post-install skips Step 2 if already done). ### `post_steps()` From 0159e3ee6b14c18b41c36ce2a5241c54fa505306 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 10:31:07 -0700 Subject: [PATCH 03/20] docs: add workday-integration solution implementation plan Co-Authored-By: Claude Sonnet 4.6 --- ...2026-08-14-workday-integration-solution.md | 648 ++++++++++++++++++ 1 file changed, 648 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-14-workday-integration-solution.md diff --git a/docs/superpowers/plans/2026-08-14-workday-integration-solution.md b/docs/superpowers/plans/2026-08-14-workday-integration-solution.md new file mode 100644 index 0000000..96eb8ba --- /dev/null +++ b/docs/superpowers/plans/2026-08-14-workday-integration-solution.md @@ -0,0 +1,648 @@ +# Workday Integration 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:** Add a `workday-integration` Cortex CLI solution that bundles a Pied Piper org hierarchy JSON report and a setup script that configures the Cortex Workday integration via API to point at it. + +**Architecture:** Static data files (`pied-piper-hierarchy.json`, `configuration.json`) live in the solution bundle and are served publicly via raw.githubusercontent.com. A `setup.py` wizard (extending `SolutionSetup`) checks for an existing Workday config, optionally backs it up, then POSTs the bundled configuration to the Cortex Workday API. No scorecards, no entity types, no external services. + +**Tech Stack:** Python 3.11+, `requests`, `SolutionSetup` base class (same pattern as `github-actions-deploy`) + +## Global Constraints + +- Solution tag: `workday-integration` +- Solution directory: `cortexapps_cli/solutions/workday-integration/` +- Report format: `ONE_EMPLOYEE_ONE_TEAM` hierarchy +- Report URL (baked into configuration.json): `https://raw.githubusercontent.com/cortexapps/cli/main/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json` +- Cortex API endpoints (from `cortexapps_cli/commands/integrations_commands/workday.py`): + - GET existing config: `GET {base_url}/api/v1/workday/default-configuration` + - Create config: `POST {base_url}/api/v1/workday/configuration` + - Delete config: `DELETE {base_url}/api/v1/workday/configurations` (note: plural) +- Backup file on replace: `~/.cortex/solutions/workday-integration/backup-config.json` +- `SolutionSetup` must be imported dynamically (same pattern as `github-actions-deploy/setup.py`) +- State file: `~/.cortex/solutions/workday-integration.json` (handled by base class) +- Credentials come from CLI session (`cortex_api_key`, `cortex_base_url` kwargs to `main()`) +- No credential prompts in `collect_prompts()` — only the replace-existing prompt +- `feat:` commit prefix for setup.py (affects CLI deliverable); `chore:` or `docs:` for data/README + +--- + +## File Map + +| File | Action | +|------|--------| +| `cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json` | Create | +| `cortexapps_cli/solutions/workday-integration/data/configuration.json` | Create | +| `cortexapps_cli/solutions/workday-integration/setup.py` | Create | +| `cortexapps_cli/solutions/workday-integration/README.md` | Create | +| `tests/test_workday_setup.py` | Create | + +--- + +## Task 1: Data files + +**Files:** +- Create: `cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json` +- Create: `cortexapps_cli/solutions/workday-integration/data/configuration.json` +- Test: `tests/test_workday_setup.py` (data validation tests only) + +**Interfaces:** +- Produces: `DATA_DIR = Path(__file__).parent / "data"` — used by Task 2's `setup.py` +- Produces: `CONFIG_FILE = DATA_DIR / "configuration.json"` — loaded and POSTed in Task 2 + +- [ ] **Step 1: Create the directory** + +```bash +mkdir -p cortexapps_cli/solutions/workday-integration/data +``` + +- [ ] **Step 2: Write `pied-piper-hierarchy.json`** + +Create `cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json` with this exact content (7 Pied Piper employees, `ONE_EMPLOYEE_ONE_TEAM` format): + +```json +{ + "Report_Entry": [ + { + "email": "erlich.bachman@piedpiper.com", + "employeeId": "100000", + "firstName": "Erlich", + "lastName": "Bachman", + "managersEmail": "erlich.bachman@piedpiper.com", + "teamId": "WORKTEAM-1-000", + "teamName": "PP: Pied Piper", + "parentTeamId": "NONE" + }, + { + "email": "richard.hendricks@piedpiper.com", + "employeeId": "100001", + "firstName": "Richard", + "lastName": "Hendricks", + "managersEmail": "erlich.bachman@piedpiper.com", + "teamId": "WORKTEAM-1-001", + "teamName": "PP: Engineering", + "parentTeamId": "WORKTEAM-1-000" + }, + { + "email": "bertram.gilfoyle@piedpiper.com", + "employeeId": "100002", + "firstName": "Bertram", + "lastName": "Gilfoyle", + "managersEmail": "richard.hendricks@piedpiper.com", + "teamId": "WORKTEAM-1-002", + "teamName": "PP: Platform", + "parentTeamId": "WORKTEAM-1-001" + }, + { + "email": "dinesh.chugtai@piedpiper.com", + "employeeId": "100003", + "firstName": "Dinesh", + "lastName": "Chugtai", + "managersEmail": "richard.hendricks@piedpiper.com", + "teamId": "WORKTEAM-1-003", + "teamName": "PP: Frontend", + "parentTeamId": "WORKTEAM-1-001" + }, + { + "email": "jared.dunn@piedpiper.com", + "employeeId": "100004", + "firstName": "Jared", + "lastName": "Dunn", + "managersEmail": "erlich.bachman@piedpiper.com", + "teamId": "WORKTEAM-1-004", + "teamName": "PP: Operations", + "parentTeamId": "WORKTEAM-1-000" + }, + { + "email": "monica.hall@piedpiper.com", + "employeeId": "100005", + "firstName": "Monica", + "lastName": "Hall", + "managersEmail": "jared.dunn@piedpiper.com", + "teamId": "WORKTEAM-1-005", + "teamName": "PP: People Ops", + "parentTeamId": "WORKTEAM-1-004" + }, + { + "email": "nelson.bighetti@piedpiper.com", + "employeeId": "100006", + "firstName": "Nelson", + "lastName": "Bighetti", + "managersEmail": "bertram.gilfoyle@piedpiper.com", + "teamId": "WORKTEAM-1-006", + "teamName": "PP: Infrastructure", + "parentTeamId": "WORKTEAM-1-002" + } + ] +} +``` + +- [ ] **Step 3: Write `configuration.json`** + +Create `cortexapps_cli/solutions/workday-integration/data/configuration.json` with this exact content: + +```json +{ + "username": "ISU_Cortex", + "ownershipReportUrl": "https://raw.githubusercontent.com/cortexapps/cli/main/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json", + "reportMappingV2": { + "email": { "columnName": "email" }, + "employeeId": { "columnName": "employeeId" }, + "firstName": { "columnName": "firstName" }, + "lastName": { "columnName": "lastName" }, + "managerEmail": { "columnName": "managersEmail" }, + "employeeRole": null, + "rootTeams": [], + "teamListFields": null, + "fallbackFields": { + "teamId": { "columnName": "teamId" }, + "teamName": { "columnName": "teamName" }, + "fieldOnParentNode": { "columnName": "teamId" }, + "fieldOnChildNode": { "columnName": "parentTeamId" } + }, + "type": "ONE_EMPLOYEE_ONE_TEAM" + }, + "password": "asdf" +} +``` + +- [ ] **Step 4: Write failing data validation tests** + +Create `tests/test_workday_setup.py` with these data-validation tests only (setup.py tests come in Task 2): + +```python +import json +from pathlib import Path + +DATA_DIR = Path("cortexapps_cli/solutions/workday-integration/data") +REPORT_URL = ( + "https://raw.githubusercontent.com/cortexapps/cli/main" + "/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json" +) + + +def test_hierarchy_json_is_valid(): + data = json.loads((DATA_DIR / "pied-piper-hierarchy.json").read_text()) + assert "Report_Entry" in data + assert len(data["Report_Entry"]) == 7 + + +def test_hierarchy_has_root_employee(): + data = json.loads((DATA_DIR / "pied-piper-hierarchy.json").read_text()) + roots = [e for e in data["Report_Entry"] if e["managersEmail"] == e["email"]] + assert len(roots) == 1 + assert roots[0]["email"] == "erlich.bachman@piedpiper.com" + + +def test_hierarchy_has_root_team(): + data = json.loads((DATA_DIR / "pied-piper-hierarchy.json").read_text()) + roots = [e for e in data["Report_Entry"] if e["parentTeamId"] == "NONE"] + assert len(roots) == 1 + assert roots[0]["teamId"] == "WORKTEAM-1-000" + + +def test_hierarchy_required_fields(): + data = json.loads((DATA_DIR / "pied-piper-hierarchy.json").read_text()) + required = {"email", "employeeId", "firstName", "lastName", "managersEmail", + "teamId", "teamName", "parentTeamId"} + for entry in data["Report_Entry"]: + assert required <= entry.keys(), f"Missing fields in entry: {entry}" + + +def test_configuration_json_is_valid(): + config = json.loads((DATA_DIR / "configuration.json").read_text()) + assert config["ownershipReportUrl"] == REPORT_URL + assert config["reportMappingV2"]["type"] == "ONE_EMPLOYEE_ONE_TEAM" + assert "password" in config + assert "username" in config + + +def test_configuration_mapping_fields(): + config = json.loads((DATA_DIR / "configuration.json").read_text()) + mapping = config["reportMappingV2"] + assert mapping["email"]["columnName"] == "email" + assert mapping["managerEmail"]["columnName"] == "managersEmail" + ff = mapping["fallbackFields"] + assert ff["fieldOnParentNode"]["columnName"] == "teamId" + assert ff["fieldOnChildNode"]["columnName"] == "parentTeamId" +``` + +- [ ] **Step 5: Run tests to verify they fail (data files don't exist yet)** + +```bash +poetry run pytest tests/test_workday_setup.py -v +``` + +Expected: FAIL (FileNotFoundError or similar — data files not created yet). + +If files are already created from Steps 2–3, the tests should PASS. That is also fine — proceed. + +- [ ] **Step 6: Run tests to verify they pass** + +```bash +poetry run pytest tests/test_workday_setup.py::test_hierarchy_json_is_valid \ + tests/test_workday_setup.py::test_configuration_json_is_valid -v +``` + +Expected: all 6 data tests PASS. + +- [ ] **Step 7: Commit** + +```bash +git add cortexapps_cli/solutions/workday-integration/data/ tests/test_workday_setup.py +git commit -m "chore: add Pied Piper hierarchy data and configuration for workday-integration solution" +``` + +--- + +## Task 2: setup.py + +**Files:** +- Create: `cortexapps_cli/solutions/workday-integration/setup.py` +- Modify: `tests/test_workday_setup.py` (add setup script tests) + +**Interfaces:** +- Consumes: `cortexapps_cli/solutions/workday-integration/data/configuration.json` (Task 1) +- Consumes: `SolutionSetup` base class from `cortexapps_cli/solutions/_lib/setup_base.py` +- Produces: `main(cortex_api_key=None, cortex_base_url=None, no_prompt=False, **kwargs)` — called by `_run_post_install_script` in `cortexapps_cli/commands/solutions.py` +- Produces: `SETUP_DESCRIPTION` module-level string — displayed by `cortex solutions install` + +**Key facts about the `_run_post_install_script` caller (do not modify this file):** +```python +# From cortexapps_cli/commands/solutions.py: +kwargs["cortex_api_key"] = client.api_key +kwargs["cortex_base_url"] = client.base_url +kwargs["no_prompt"] = no_prompt +module.main(**kwargs) +``` +So `main()` receives `cortex_api_key`, `cortex_base_url`, and `no_prompt` as keyword args. + +**Cortex API endpoints to call:** +- Check existing: `GET {base_url}/api/v1/workday/default-configuration` +- Delete existing: `DELETE {base_url}/api/v1/workday/configurations` (plural `configurations`) +- Create new: `POST {base_url}/api/v1/workday/configuration` (singular `configuration`) + +- [ ] **Step 1: Write failing tests for the setup script** + +Append to `tests/test_workday_setup.py`: + +```python +import importlib.util +import json +import pytest +from pathlib import Path +from unittest.mock import patch, MagicMock + + +def load_setup_module(): + spec = importlib.util.spec_from_file_location( + "workday_setup", + "cortexapps_cli/solutions/workday-integration/setup.py", + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +@pytest.fixture +def mod(): + return load_setup_module() + + +@pytest.fixture +def setup(mod, tmp_path): + return mod.WorkdayIntegrationSetup( + cortex_api_key="crt_test", + cortex_base_url="https://api.getcortexapp.com", + state_dir=tmp_path, + ) + + +def test_solution_tag(mod): + assert mod.WorkdayIntegrationSetup.solution_tag == "workday-integration" + + +def test_setup_description(mod): + assert "Workday" in mod.SETUP_DESCRIPTION + + +def test_collect_prompts_is_noop(setup): + # collect_prompts() must not raise and must not call input() + with patch("builtins.input", side_effect=AssertionError("should not prompt")): + setup.collect_prompts() # no exception = pass + + +def test_check_existing_no_config_proceeds(setup): + resp_404 = MagicMock(status_code=404) + resp_404.raise_for_status = MagicMock() + with patch("requests.get", return_value=resp_404): + # Should return without prompting or raising + setup._check_and_replace_existing() + + +def test_check_existing_user_declines_exits(setup): + resp_200 = MagicMock(status_code=200) + resp_200.raise_for_status = MagicMock() + resp_200.json.return_value = {"username": "ISU_Cortex"} + with patch("requests.get", return_value=resp_200), \ + patch("builtins.input", return_value="n"): + with pytest.raises(SystemExit) as exc_info: + setup._check_and_replace_existing() + assert exc_info.value.code == 0 + + +def test_check_existing_user_accepts_backs_up_and_deletes(setup, tmp_path): + existing = {"username": "ISU_Cortex", "ownershipReportUrl": "https://old.example.com"} + resp_200 = MagicMock(status_code=200) + resp_200.raise_for_status = MagicMock() + resp_200.json.return_value = existing + resp_del = MagicMock(status_code=204) + resp_del.raise_for_status = MagicMock() + + backup_dir = tmp_path / "workday-integration" + + with patch("requests.get", return_value=resp_200), \ + patch("requests.delete", return_value=resp_del) as mock_delete, \ + patch("builtins.input", return_value="y"), \ + patch("pathlib.Path.home", return_value=tmp_path): + setup._check_and_replace_existing() + + mock_delete.assert_called_once() + delete_url = mock_delete.call_args.args[0] + assert "configurations" in delete_url # plural endpoint + + backup_file = tmp_path / ".cortex" / "solutions" / "workday-integration" / "backup-config.json" + assert backup_file.exists() + assert json.loads(backup_file.read_text()) == existing + + +def test_configure_integration_posts_correct_payload(setup): + resp = MagicMock(ok=True, status_code=200) + with patch("requests.post", return_value=resp) as mock_post: + setup._configure_integration() + + mock_post.assert_called_once() + url = mock_post.call_args.args[0] + assert url.endswith("/api/v1/workday/configuration") + + payload = mock_post.call_args.kwargs["json"] + assert payload["reportMappingV2"]["type"] == "ONE_EMPLOYEE_ONE_TEAM" + assert "pied-piper-hierarchy.json" in payload["ownershipReportUrl"] + + +def test_configure_integration_raises_on_failure(setup): + resp = MagicMock(ok=False, status_code=400, text="Bad Request") + with patch("requests.post", return_value=resp): + with pytest.raises(RuntimeError, match="Failed to configure"): + setup._configure_integration() + + +def test_configure_integration_idempotent(setup, tmp_path): + setup.mark_done("configure") + with patch("requests.post") as mock_post: + setup._configure_integration() + mock_post.assert_not_called() + + +def test_main_callable(mod): + assert callable(mod.main) +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +poetry run pytest tests/test_workday_setup.py -k "not test_hierarchy and not test_configuration" -v +``` + +Expected: FAIL with `ModuleNotFoundError` or `FileNotFoundError` (setup.py doesn't exist yet). + +- [ ] **Step 3: Create `setup.py`** + +Create `cortexapps_cli/solutions/workday-integration/setup.py` with this exact content: + +```python +""" +Post-install setup script for the workday-integration solution. +Configures the Cortex Workday integration to sync the Pied Piper org hierarchy. +Run via: cortex solutions post-install -s workday-integration +""" + +SETUP_DESCRIPTION = ( + "This solution includes a post-install setup script that will configure " + "the Cortex Workday integration to sync the Pied Piper org hierarchy." +) + +import json +import sys +from pathlib import Path +import requests + +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 + +DATA_DIR = Path(__file__).parent / "data" +CONFIG_FILE = DATA_DIR / "configuration.json" + + +class WorkdayIntegrationSetup(SolutionSetup): + solution_tag = "workday-integration" + + 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._api_key = cortex_api_key or "" + self._base_url = (cortex_base_url or "https://api.getcortexapp.com").rstrip("/") + + def _cortex_headers(self) -> dict: + return { + "Authorization": f"Bearer {self._api_key}", + "Content-Type": "application/json", + } + + def collect_prompts(self) -> None: + pass # credentials come from CLI session + + def _check_and_replace_existing(self) -> None: + """Check for an existing Workday config; backup and delete it if user confirms.""" + r = requests.get( + f"{self._base_url}/api/v1/workday/default-configuration", + headers=self._cortex_headers(), + ) + if r.status_code == 404: + return # no existing config — proceed + r.raise_for_status() + + if not self.confirm("Existing Workday integration found. Replace it?", default=False): + print("Keeping existing Workday integration. Exiting.") + raise SystemExit(0) + + # Back up the existing config + backup_dir = Path.home() / ".cortex" / "solutions" / "workday-integration" + backup_dir.mkdir(parents=True, exist_ok=True) + backup_file = backup_dir / "backup-config.json" + backup_file.write_text(json.dumps(r.json(), indent=2)) + print(f" Backed up existing config to {backup_file}") + + # Delete the existing config + del_r = requests.delete( + f"{self._base_url}/api/v1/workday/configurations", + headers=self._cortex_headers(), + ) + del_r.raise_for_status() + + def _configure_integration(self) -> None: + """POST the bundled Workday integration configuration.""" + if self.already_done("configure"): + return "Already configured (skipped)" + config = json.loads(CONFIG_FILE.read_text()) + r = requests.post( + f"{self._base_url}/api/v1/workday/configuration", + headers=self._cortex_headers(), + json=config, + ) + if not r.ok: + raise RuntimeError( + f"Failed to configure Workday integration: {r.status_code} {r.text}" + ) + self.mark_done("configure") + + def steps(self) -> list: + return [ + ("Check for existing Workday integration", self._check_and_replace_existing), + ("Configure Workday integration", self._configure_integration), + ] + + def post_steps(self) -> None: + print("\n✓ Workday integration configured with the Pied Piper org hierarchy.\n") + print("Next: trigger the import in Cortex:") + print(" Catalog → All Entities → Import Entities\n") + print("Then check your team hierarchy to see the Pied Piper org chart.") + + +def main(cortex_api_key=None, cortex_base_url=None, no_prompt=False, **kwargs): + WorkdayIntegrationSetup( + cortex_api_key=cortex_api_key, + cortex_base_url=cortex_base_url, + no_prompt=no_prompt, + ).run() + + +if __name__ == "__main__": + main() +``` + +- [ ] **Step 4: Run all setup script tests** + +```bash +poetry run pytest tests/test_workday_setup.py -v +``` + +Expected: all tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add cortexapps_cli/solutions/workday-integration/setup.py tests/test_workday_setup.py +git commit -m "feat: add workday-integration solution setup script" +``` + +--- + +## Task 3: README + +**Files:** +- Create: `cortexapps_cli/solutions/workday-integration/README.md` + +**Interfaces:** +- Consumes: nothing from prior tasks (pure documentation) + +- [ ] **Step 1: Create `README.md`** + +Create `cortexapps_cli/solutions/workday-integration/README.md` with this content: + +```markdown +--- +name: Workday Integration +description: Configure the Cortex Workday integration with a sample Pied Piper org hierarchy to sync employees and teams into your service catalog. +--- + +# Workday Integration + +Get the Cortex Workday integration running in minutes using a pre-built Pied Piper org hierarchy. After install, trigger a sync to see employees and teams appear in your catalog — including the full team hierarchy. + +## What's Included + +- **Pied Piper org data:** 7 employees across 4 levels of hierarchy (Erlich → Richard → Gilfoyle/Dinesh, Jared → Monica, Bachman → Big Head) +- **Integration config:** field mapping and report URL pre-configured, pointing at the hosted data +- **Setup script:** one-command configuration of the Cortex Workday integration via API + +## Quick Start + +1. Install the solution: + + ``` + cortex solutions install -s workday-integration + ``` + +2. Follow the post-install setup prompts, or run later: + + ``` + cortex solutions post-install -s workday-integration + ``` + +3. Trigger the import in Cortex: + + **Catalog → All Entities → Import Entities** + +4. Check your team hierarchy to see the Pied Piper org chart. + +## Org Hierarchy + +``` +PP: Pied Piper (Erlich Bachman) +├── PP: Engineering (Richard Hendricks) +│ ├── PP: Platform (Bertram Gilfoyle) +│ │ └── PP: Infrastructure (Nelson Bighetti) +│ └── PP: Frontend (Dinesh Chugtai) +└── PP: Operations (Jared Dunn) + └── PP: People Ops (Monica Hall) +``` + +## How It Works + +The setup script calls the Cortex Workday integration API to configure a report URL pointing at `pied-piper-hierarchy.json` hosted in this repository. Cortex fetches the report and syncs employees and teams into your catalog on the next import run. + +## Adapting to Real Workday Data + +To point the integration at a real Workday RaaS report: + +1. Go to **Settings → Integrations → Workday** in the Cortex UI +2. Update the **Report URL** to your Workday RaaS endpoint +3. Set your real **username** and **password** +4. Trigger a new import + +The field mapping (`reportMappingV2`) in `data/configuration.json` matches the standard Cortex Workday report format and works unchanged for real Workday data that uses the same column names. +``` + +- [ ] **Step 2: Run the full test suite to confirm nothing is broken** + +```bash +poetry run pytest tests/test_workday_setup.py tests/test_solutions_postinstall.py tests/test_setup_base.py -v +``` + +Expected: all tests PASS. + +- [ ] **Step 3: Commit** + +```bash +git add cortexapps_cli/solutions/workday-integration/README.md +git commit -m "docs: add README for workday-integration solution" +``` From 6be798bf364178c6c96f4ea966723f2c30f2dd53 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 10:36:42 -0700 Subject: [PATCH 04/20] fix: apply test fixes and resolve rebase conflict in github-actions-deploy setup Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/solutions/github-actions-deploy/setup.py | 2 -- tests/test_github_actions_setup.py | 2 +- tests/test_setup_base.py | 5 +++-- tests/test_solutions_postinstall.py | 8 ++++---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/cortexapps_cli/solutions/github-actions-deploy/setup.py b/cortexapps_cli/solutions/github-actions-deploy/setup.py index d6f601a..b0e2f36 100644 --- a/cortexapps_cli/solutions/github-actions-deploy/setup.py +++ b/cortexapps_cli/solutions/github-actions-deploy/setup.py @@ -172,8 +172,6 @@ def collect_prompts(self) -> None: if self._session_base_url: self._answers["cortex_base_url"] = self._session_base_url - else: - self.prompt("cortex_base_url", "Cortex base URL", default=self._session_base_url) else: self.prompt( "cortex_base_url", diff --git a/tests/test_github_actions_setup.py b/tests/test_github_actions_setup.py index fa83ef3..8ea4a80 100644 --- a/tests/test_github_actions_setup.py +++ b/tests/test_github_actions_setup.py @@ -123,7 +123,7 @@ def test_set_secret(setup): def test_trigger_workflow(setup): resp = MagicMock(status_code=204) with patch("requests.post", return_value=resp) as mock_post: - setup._trigger_workflow() + setup._trigger_direct() url = mock_post.call_args.args[0] assert "dispatches" in url assert mock_post.call_args.kwargs["json"] == {"ref": "main"} diff --git a/tests/test_setup_base.py b/tests/test_setup_base.py index e88d8d3..60092b3 100644 --- a/tests/test_setup_base.py +++ b/tests/test_setup_base.py @@ -19,7 +19,8 @@ def steps(self): def test_prompt_uses_env_var(tmp_path, monkeypatch): monkeypatch.setenv("MY_VAR", "from-env") setup = ConcreteSetup(state_dir=tmp_path) - result = setup.prompt("key", "Enter value", env_var="MY_VAR") + with patch("builtins.input", return_value=""): + result = setup.prompt("key", "Enter value", env_var="MY_VAR") assert result == "from-env" @@ -77,7 +78,7 @@ def test_mark_done_persists_across_instances(tmp_path): def test_state_file_path(tmp_path): setup = ConcreteSetup(state_dir=tmp_path) - assert setup._state_file == tmp_path / "setup-test-solution.json" + assert setup._state_file == tmp_path / "test-solution.json" def test_post_steps_called_after_steps(tmp_path): diff --git a/tests/test_solutions_postinstall.py b/tests/test_solutions_postinstall.py index 5c71552..98deb6e 100644 --- a/tests/test_solutions_postinstall.py +++ b/tests/test_solutions_postinstall.py @@ -1,5 +1,5 @@ import pytest -from unittest.mock import patch, MagicMock +from unittest.mock import patch, MagicMock, ANY from typer.testing import CliRunner from cortexapps_cli.cli import app @@ -21,7 +21,7 @@ def test_post_install_unknown_solution(): def test_post_install_calls_run_for_github_actions(): with patch("cortexapps_cli.commands.solutions._run_post_install_script") as mock_run: runner.invoke(app, ["solutions", "post-install", "-s", "github-actions-deploy"]) - mock_run.assert_called_once_with("github-actions-deploy", solutions_dir=None) + mock_run.assert_called_once_with("github-actions-deploy", solutions_dir=None, ctx=ANY, no_prompt=False) def test_install_skip_post_install_setup_flag_skips_script(): @@ -51,5 +51,5 @@ def test_install_prompts_and_runs_post_install_on_yes(): ["-k", "fake", "solutions", "install", "-s", "github-actions-deploy"], input="y\n", ) - assert "This solution includes a post-install setup script." in result.output - mock_run.assert_called_once_with("github-actions-deploy", solutions_dir=None) + assert "This solution includes a post-install setup script" in result.output + mock_run.assert_called_once_with("github-actions-deploy", solutions_dir=None, ctx=ANY) From e260ead2ca7e1124aff3c9f12a2beb7d89dff5b5 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 10:38:32 -0700 Subject: [PATCH 05/20] chore: add Pied Piper hierarchy data and configuration for workday-integration solution --- .../data/configuration.json | 22 ++++++ .../data/pied-piper-hierarchy.json | 74 +++++++++++++++++++ tests/test_workday_setup.py | 54 ++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 cortexapps_cli/solutions/workday-integration/data/configuration.json create mode 100644 cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json create mode 100644 tests/test_workday_setup.py diff --git a/cortexapps_cli/solutions/workday-integration/data/configuration.json b/cortexapps_cli/solutions/workday-integration/data/configuration.json new file mode 100644 index 0000000..ba618d8 --- /dev/null +++ b/cortexapps_cli/solutions/workday-integration/data/configuration.json @@ -0,0 +1,22 @@ +{ + "username": "ISU_Cortex", + "ownershipReportUrl": "https://raw.githubusercontent.com/cortexapps/cli/main/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json", + "reportMappingV2": { + "email": { "columnName": "email" }, + "employeeId": { "columnName": "employeeId" }, + "firstName": { "columnName": "firstName" }, + "lastName": { "columnName": "lastName" }, + "managerEmail": { "columnName": "managersEmail" }, + "employeeRole": null, + "rootTeams": [], + "teamListFields": null, + "fallbackFields": { + "teamId": { "columnName": "teamId" }, + "teamName": { "columnName": "teamName" }, + "fieldOnParentNode": { "columnName": "teamId" }, + "fieldOnChildNode": { "columnName": "parentTeamId" } + }, + "type": "ONE_EMPLOYEE_ONE_TEAM" + }, + "password": "asdf" +} diff --git a/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json b/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json new file mode 100644 index 0000000..3f96ee7 --- /dev/null +++ b/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json @@ -0,0 +1,74 @@ +{ + "Report_Entry": [ + { + "email": "erlich.bachman@piedpiper.com", + "employeeId": "100000", + "firstName": "Erlich", + "lastName": "Bachman", + "managersEmail": "erlich.bachman@piedpiper.com", + "teamId": "WORKTEAM-1-000", + "teamName": "PP: Pied Piper", + "parentTeamId": "NONE" + }, + { + "email": "richard.hendricks@piedpiper.com", + "employeeId": "100001", + "firstName": "Richard", + "lastName": "Hendricks", + "managersEmail": "erlich.bachman@piedpiper.com", + "teamId": "WORKTEAM-1-001", + "teamName": "PP: Engineering", + "parentTeamId": "WORKTEAM-1-000" + }, + { + "email": "bertram.gilfoyle@piedpiper.com", + "employeeId": "100002", + "firstName": "Bertram", + "lastName": "Gilfoyle", + "managersEmail": "richard.hendricks@piedpiper.com", + "teamId": "WORKTEAM-1-002", + "teamName": "PP: Platform", + "parentTeamId": "WORKTEAM-1-001" + }, + { + "email": "dinesh.chugtai@piedpiper.com", + "employeeId": "100003", + "firstName": "Dinesh", + "lastName": "Chugtai", + "managersEmail": "richard.hendricks@piedpiper.com", + "teamId": "WORKTEAM-1-003", + "teamName": "PP: Frontend", + "parentTeamId": "WORKTEAM-1-001" + }, + { + "email": "jared.dunn@piedpiper.com", + "employeeId": "100004", + "firstName": "Jared", + "lastName": "Dunn", + "managersEmail": "erlich.bachman@piedpiper.com", + "teamId": "WORKTEAM-1-004", + "teamName": "PP: Operations", + "parentTeamId": "WORKTEAM-1-000" + }, + { + "email": "monica.hall@piedpiper.com", + "employeeId": "100005", + "firstName": "Monica", + "lastName": "Hall", + "managersEmail": "jared.dunn@piedpiper.com", + "teamId": "WORKTEAM-1-005", + "teamName": "PP: People Ops", + "parentTeamId": "WORKTEAM-1-004" + }, + { + "email": "nelson.bighetti@piedpiper.com", + "employeeId": "100006", + "firstName": "Nelson", + "lastName": "Bighetti", + "managersEmail": "bertram.gilfoyle@piedpiper.com", + "teamId": "WORKTEAM-1-006", + "teamName": "PP: Infrastructure", + "parentTeamId": "WORKTEAM-1-002" + } + ] +} diff --git a/tests/test_workday_setup.py b/tests/test_workday_setup.py new file mode 100644 index 0000000..e6dee62 --- /dev/null +++ b/tests/test_workday_setup.py @@ -0,0 +1,54 @@ +import json +from pathlib import Path + +DATA_DIR = Path("cortexapps_cli/solutions/workday-integration/data") +REPORT_URL = ( + "https://raw.githubusercontent.com/cortexapps/cli/main" + "/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json" +) + + +def test_hierarchy_json_is_valid(): + data = json.loads((DATA_DIR / "pied-piper-hierarchy.json").read_text()) + assert "Report_Entry" in data + assert len(data["Report_Entry"]) == 7 + + +def test_hierarchy_has_root_employee(): + data = json.loads((DATA_DIR / "pied-piper-hierarchy.json").read_text()) + roots = [e for e in data["Report_Entry"] if e["managersEmail"] == e["email"]] + assert len(roots) == 1 + assert roots[0]["email"] == "erlich.bachman@piedpiper.com" + + +def test_hierarchy_has_root_team(): + data = json.loads((DATA_DIR / "pied-piper-hierarchy.json").read_text()) + roots = [e for e in data["Report_Entry"] if e["parentTeamId"] == "NONE"] + assert len(roots) == 1 + assert roots[0]["teamId"] == "WORKTEAM-1-000" + + +def test_hierarchy_required_fields(): + data = json.loads((DATA_DIR / "pied-piper-hierarchy.json").read_text()) + required = {"email", "employeeId", "firstName", "lastName", "managersEmail", + "teamId", "teamName", "parentTeamId"} + for entry in data["Report_Entry"]: + assert required <= entry.keys(), f"Missing fields in entry: {entry}" + + +def test_configuration_json_is_valid(): + config = json.loads((DATA_DIR / "configuration.json").read_text()) + assert config["ownershipReportUrl"] == REPORT_URL + assert config["reportMappingV2"]["type"] == "ONE_EMPLOYEE_ONE_TEAM" + assert "password" in config + assert "username" in config + + +def test_configuration_mapping_fields(): + config = json.loads((DATA_DIR / "configuration.json").read_text()) + mapping = config["reportMappingV2"] + assert mapping["email"]["columnName"] == "email" + assert mapping["managerEmail"]["columnName"] == "managersEmail" + ff = mapping["fallbackFields"] + assert ff["fieldOnParentNode"]["columnName"] == "teamId" + assert ff["fieldOnChildNode"]["columnName"] == "parentTeamId" From 0671d34dbdc693aa7ae0be5d894e717f22a9b67c Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 10:41:12 -0700 Subject: [PATCH 06/20] feat: add workday-integration solution setup script Co-Authored-By: Claude Sonnet 4.6 --- .../solutions/workday-integration/setup.py | 116 +++++++++++++++++ tests/test_workday_setup.py | 119 ++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 cortexapps_cli/solutions/workday-integration/setup.py diff --git a/cortexapps_cli/solutions/workday-integration/setup.py b/cortexapps_cli/solutions/workday-integration/setup.py new file mode 100644 index 0000000..325a36d --- /dev/null +++ b/cortexapps_cli/solutions/workday-integration/setup.py @@ -0,0 +1,116 @@ +""" +Post-install setup script for the workday-integration solution. +Configures the Cortex Workday integration to sync the Pied Piper org hierarchy. +Run via: cortex solutions post-install -s workday-integration +""" + +SETUP_DESCRIPTION = ( + "This solution includes a post-install setup script that will configure " + "the Cortex Workday integration to sync the Pied Piper org hierarchy." +) + +import json +import sys +from pathlib import Path +import requests + +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 + +DATA_DIR = Path(__file__).parent / "data" +CONFIG_FILE = DATA_DIR / "configuration.json" + + +class WorkdayIntegrationSetup(SolutionSetup): + solution_tag = "workday-integration" + + 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._api_key = cortex_api_key or "" + self._base_url = (cortex_base_url or "https://api.getcortexapp.com").rstrip("/") + + def _cortex_headers(self) -> dict: + return { + "Authorization": f"Bearer {self._api_key}", + "Content-Type": "application/json", + } + + def collect_prompts(self) -> None: + pass # credentials come from CLI session + + def _check_and_replace_existing(self) -> None: + """Check for an existing Workday config; backup and delete it if user confirms.""" + r = requests.get( + f"{self._base_url}/api/v1/workday/default-configuration", + headers=self._cortex_headers(), + ) + if r.status_code == 404: + return # no existing config — proceed + r.raise_for_status() + + if not self.confirm("Existing Workday integration found. Replace it?", default=False): + print("Keeping existing Workday integration. Exiting.") + raise SystemExit(0) + + # Back up the existing config + backup_dir = Path.home() / ".cortex" / "solutions" / "workday-integration" + backup_dir.mkdir(parents=True, exist_ok=True) + backup_file = backup_dir / "backup-config.json" + backup_file.write_text(json.dumps(r.json(), indent=2)) + print(f" Backed up existing config to {backup_file}") + + # Delete the existing config + del_r = requests.delete( + f"{self._base_url}/api/v1/workday/configurations", + headers=self._cortex_headers(), + ) + del_r.raise_for_status() + + def _configure_integration(self) -> None: + """POST the bundled Workday integration configuration.""" + if self.already_done("configure"): + return "Already configured (skipped)" + config = json.loads(CONFIG_FILE.read_text()) + r = requests.post( + f"{self._base_url}/api/v1/workday/configuration", + headers=self._cortex_headers(), + json=config, + ) + if not r.ok: + raise RuntimeError( + f"Failed to configure Workday integration: {r.status_code} {r.text}" + ) + self.mark_done("configure") + + def steps(self) -> list: + return [ + ("Check for existing Workday integration", self._check_and_replace_existing), + ("Configure Workday integration", self._configure_integration), + ] + + def post_steps(self) -> None: + print("\n✓ Workday integration configured with the Pied Piper org hierarchy.\n") + print("Next: trigger the import in Cortex:") + print(" Catalog → All Entities → Import Entities\n") + print("Then check your team hierarchy to see the Pied Piper org chart.") + + +def main(cortex_api_key=None, cortex_base_url=None, no_prompt=False, **kwargs): + WorkdayIntegrationSetup( + cortex_api_key=cortex_api_key, + cortex_base_url=cortex_base_url, + no_prompt=no_prompt, + ).run() + + +if __name__ == "__main__": + main() diff --git a/tests/test_workday_setup.py b/tests/test_workday_setup.py index e6dee62..11285d2 100644 --- a/tests/test_workday_setup.py +++ b/tests/test_workday_setup.py @@ -52,3 +52,122 @@ def test_configuration_mapping_fields(): ff = mapping["fallbackFields"] assert ff["fieldOnParentNode"]["columnName"] == "teamId" assert ff["fieldOnChildNode"]["columnName"] == "parentTeamId" + + +import importlib.util +import pytest +from unittest.mock import patch, MagicMock + + +def load_setup_module(): + spec = importlib.util.spec_from_file_location( + "workday_setup", + "cortexapps_cli/solutions/workday-integration/setup.py", + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +@pytest.fixture +def mod(): + return load_setup_module() + + +@pytest.fixture +def setup(mod, tmp_path): + return mod.WorkdayIntegrationSetup( + cortex_api_key="crt_test", + cortex_base_url="https://api.getcortexapp.com", + state_dir=tmp_path, + ) + + +def test_solution_tag(mod): + assert mod.WorkdayIntegrationSetup.solution_tag == "workday-integration" + + +def test_setup_description(mod): + assert "Workday" in mod.SETUP_DESCRIPTION + + +def test_collect_prompts_is_noop(setup): + # collect_prompts() must not raise and must not call input() + with patch("builtins.input", side_effect=AssertionError("should not prompt")): + setup.collect_prompts() # no exception = pass + + +def test_check_existing_no_config_proceeds(setup): + resp_404 = MagicMock(status_code=404) + resp_404.raise_for_status = MagicMock() + with patch("requests.get", return_value=resp_404): + # Should return without prompting or raising + setup._check_and_replace_existing() + + +def test_check_existing_user_declines_exits(setup): + resp_200 = MagicMock(status_code=200) + resp_200.raise_for_status = MagicMock() + resp_200.json.return_value = {"username": "ISU_Cortex"} + with patch("requests.get", return_value=resp_200), \ + patch("builtins.input", return_value="n"): + with pytest.raises(SystemExit) as exc_info: + setup._check_and_replace_existing() + assert exc_info.value.code == 0 + + +def test_check_existing_user_accepts_backs_up_and_deletes(setup, tmp_path): + existing = {"username": "ISU_Cortex", "ownershipReportUrl": "https://old.example.com"} + resp_200 = MagicMock(status_code=200) + resp_200.raise_for_status = MagicMock() + resp_200.json.return_value = existing + resp_del = MagicMock(status_code=204) + resp_del.raise_for_status = MagicMock() + + backup_dir = tmp_path / "workday-integration" + + with patch("requests.get", return_value=resp_200), \ + patch("requests.delete", return_value=resp_del) as mock_delete, \ + patch("builtins.input", return_value="y"), \ + patch("pathlib.Path.home", return_value=tmp_path): + setup._check_and_replace_existing() + + mock_delete.assert_called_once() + delete_url = mock_delete.call_args.args[0] + assert "configurations" in delete_url # plural endpoint + + backup_file = tmp_path / ".cortex" / "solutions" / "workday-integration" / "backup-config.json" + assert backup_file.exists() + assert json.loads(backup_file.read_text()) == existing + + +def test_configure_integration_posts_correct_payload(setup): + resp = MagicMock(ok=True, status_code=200) + with patch("requests.post", return_value=resp) as mock_post: + setup._configure_integration() + + mock_post.assert_called_once() + url = mock_post.call_args.args[0] + assert url.endswith("/api/v1/workday/configuration") + + payload = mock_post.call_args.kwargs["json"] + assert payload["reportMappingV2"]["type"] == "ONE_EMPLOYEE_ONE_TEAM" + assert "pied-piper-hierarchy.json" in payload["ownershipReportUrl"] + + +def test_configure_integration_raises_on_failure(setup): + resp = MagicMock(ok=False, status_code=400, text="Bad Request") + with patch("requests.post", return_value=resp): + with pytest.raises(RuntimeError, match="Failed to configure"): + setup._configure_integration() + + +def test_configure_integration_idempotent(setup, tmp_path): + setup.mark_done("configure") + with patch("requests.post") as mock_post: + setup._configure_integration() + mock_post.assert_not_called() + + +def test_main_callable(mod): + assert callable(mod.main) From 8af34b723fa10bb23d028c6ff19d46bfb77bc191 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 10:43:30 -0700 Subject: [PATCH 07/20] docs: add README for workday-integration solution --- .../solutions/workday-integration/README.md | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 cortexapps_cli/solutions/workday-integration/README.md diff --git a/cortexapps_cli/solutions/workday-integration/README.md b/cortexapps_cli/solutions/workday-integration/README.md new file mode 100644 index 0000000..f7bd385 --- /dev/null +++ b/cortexapps_cli/solutions/workday-integration/README.md @@ -0,0 +1,61 @@ +--- +name: Workday Integration +description: Configure the Cortex Workday integration with a sample Pied Piper org hierarchy to sync employees and teams into your service catalog. +--- + +# Workday Integration + +Get the Cortex Workday integration running in minutes using a pre-built Pied Piper org hierarchy. After install, trigger a sync to see employees and teams appear in your catalog — including the full team hierarchy. + +## What's Included + +- **Pied Piper org data:** 7 employees across 4 levels of hierarchy (Erlich → Richard → Gilfoyle/Dinesh, Jared → Monica, Bachman → Big Head) +- **Integration config:** field mapping and report URL pre-configured, pointing at the hosted data +- **Setup script:** one-command configuration of the Cortex Workday integration via API + +## Quick Start + +1. Install the solution: + + ``` + cortex solutions install -s workday-integration + ``` + +2. Follow the post-install setup prompts, or run later: + + ``` + cortex solutions post-install -s workday-integration + ``` + +3. Trigger the import in Cortex: + + **Catalog → All Entities → Import Entities** + +4. Check your team hierarchy to see the Pied Piper org chart. + +## Org Hierarchy + +``` +PP: Pied Piper (Erlich Bachman) +├── PP: Engineering (Richard Hendricks) +│ ├── PP: Platform (Bertram Gilfoyle) +│ │ └── PP: Infrastructure (Nelson Bighetti) +│ └── PP: Frontend (Dinesh Chugtai) +└── PP: Operations (Jared Dunn) + └── PP: People Ops (Monica Hall) +``` + +## How It Works + +The setup script calls the Cortex Workday integration API to configure a report URL pointing at `pied-piper-hierarchy.json` hosted in this repository. Cortex fetches the report and syncs employees and teams into your catalog on the next import run. + +## Adapting to Real Workday Data + +To point the integration at a real Workday RaaS report: + +1. Go to **Settings → Integrations → Workday** in the Cortex UI +2. Update the **Report URL** to your Workday RaaS endpoint +3. Set your real **username** and **password** +4. Trigger a new import + +The field mapping (`reportMappingV2`) in `data/configuration.json` matches the standard Cortex Workday report format and works unchanged for real Workday data that uses the same column names. From 74585281de6131db0b29190cf5be76892e6f1ede Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 10:55:39 -0700 Subject: [PATCH 08/20] chore: rename solution slug from workday-integration to workday MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - git mv cortexapps_cli/solutions/workday-integration → workday - Update solution_tag, backup_dir, and docstring in setup.py - Update ownershipReportUrl in configuration.json - Update CLI commands in README.md - Add Silicon Valley attribution to README description text - Update all workday-integration references in test_workday_setup.py Co-Authored-By: Claude Sonnet 4.6 --- .../{workday-integration => workday}/README.md | 8 ++++---- .../data/configuration.json | 2 +- .../data/pied-piper-hierarchy.json | 0 .../{workday-integration => workday}/setup.py | 8 ++++---- tests/test_workday_setup.py | 12 ++++++------ 5 files changed, 15 insertions(+), 15 deletions(-) rename cortexapps_cli/solutions/{workday-integration => workday}/README.md (78%) rename cortexapps_cli/solutions/{workday-integration => workday}/data/configuration.json (88%) rename cortexapps_cli/solutions/{workday-integration => workday}/data/pied-piper-hierarchy.json (100%) rename cortexapps_cli/solutions/{workday-integration => workday}/setup.py (95%) diff --git a/cortexapps_cli/solutions/workday-integration/README.md b/cortexapps_cli/solutions/workday/README.md similarity index 78% rename from cortexapps_cli/solutions/workday-integration/README.md rename to cortexapps_cli/solutions/workday/README.md index f7bd385..0dc0d1b 100644 --- a/cortexapps_cli/solutions/workday-integration/README.md +++ b/cortexapps_cli/solutions/workday/README.md @@ -1,11 +1,11 @@ --- name: Workday Integration -description: Configure the Cortex Workday integration with a sample Pied Piper org hierarchy to sync employees and teams into your service catalog. +description: Configure the Cortex Workday integration with a sample org hierarchy from the fictional company Pied Piper (from the TV show Silicon Valley) to sync employees and teams into your service catalog. --- # Workday Integration -Get the Cortex Workday integration running in minutes using a pre-built Pied Piper org hierarchy. After install, trigger a sync to see employees and teams appear in your catalog — including the full team hierarchy. +Get the Cortex Workday integration running in minutes using a sample org hierarchy from the fictional company Pied Piper (from the TV show *Silicon Valley*). After install, trigger a sync to see employees and teams appear in your catalog — including the full team hierarchy. ## What's Included @@ -18,13 +18,13 @@ Get the Cortex Workday integration running in minutes using a pre-built Pied Pip 1. Install the solution: ``` - cortex solutions install -s workday-integration + cortex solutions install -s workday ``` 2. Follow the post-install setup prompts, or run later: ``` - cortex solutions post-install -s workday-integration + cortex solutions post-install -s workday ``` 3. Trigger the import in Cortex: diff --git a/cortexapps_cli/solutions/workday-integration/data/configuration.json b/cortexapps_cli/solutions/workday/data/configuration.json similarity index 88% rename from cortexapps_cli/solutions/workday-integration/data/configuration.json rename to cortexapps_cli/solutions/workday/data/configuration.json index ba618d8..cde8c35 100644 --- a/cortexapps_cli/solutions/workday-integration/data/configuration.json +++ b/cortexapps_cli/solutions/workday/data/configuration.json @@ -1,6 +1,6 @@ { "username": "ISU_Cortex", - "ownershipReportUrl": "https://raw.githubusercontent.com/cortexapps/cli/main/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json", + "ownershipReportUrl": "https://raw.githubusercontent.com/cortexapps/cli/main/cortexapps_cli/solutions/workday/data/pied-piper-hierarchy.json", "reportMappingV2": { "email": { "columnName": "email" }, "employeeId": { "columnName": "employeeId" }, diff --git a/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json b/cortexapps_cli/solutions/workday/data/pied-piper-hierarchy.json similarity index 100% rename from cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json rename to cortexapps_cli/solutions/workday/data/pied-piper-hierarchy.json diff --git a/cortexapps_cli/solutions/workday-integration/setup.py b/cortexapps_cli/solutions/workday/setup.py similarity index 95% rename from cortexapps_cli/solutions/workday-integration/setup.py rename to cortexapps_cli/solutions/workday/setup.py index 325a36d..fc8f95d 100644 --- a/cortexapps_cli/solutions/workday-integration/setup.py +++ b/cortexapps_cli/solutions/workday/setup.py @@ -1,7 +1,7 @@ """ -Post-install setup script for the workday-integration solution. +Post-install setup script for the workday solution. Configures the Cortex Workday integration to sync the Pied Piper org hierarchy. -Run via: cortex solutions post-install -s workday-integration +Run via: cortex solutions post-install -s workday """ SETUP_DESCRIPTION = ( @@ -25,7 +25,7 @@ class WorkdayIntegrationSetup(SolutionSetup): - solution_tag = "workday-integration" + solution_tag = "workday" def __init__( self, @@ -62,7 +62,7 @@ def _check_and_replace_existing(self) -> None: raise SystemExit(0) # Back up the existing config - backup_dir = Path.home() / ".cortex" / "solutions" / "workday-integration" + backup_dir = Path.home() / ".cortex" / "solutions" / "workday" backup_dir.mkdir(parents=True, exist_ok=True) backup_file = backup_dir / "backup-config.json" backup_file.write_text(json.dumps(r.json(), indent=2)) diff --git a/tests/test_workday_setup.py b/tests/test_workday_setup.py index 11285d2..014b104 100644 --- a/tests/test_workday_setup.py +++ b/tests/test_workday_setup.py @@ -1,10 +1,10 @@ import json from pathlib import Path -DATA_DIR = Path("cortexapps_cli/solutions/workday-integration/data") +DATA_DIR = Path("cortexapps_cli/solutions/workday/data") REPORT_URL = ( "https://raw.githubusercontent.com/cortexapps/cli/main" - "/cortexapps_cli/solutions/workday-integration/data/pied-piper-hierarchy.json" + "/cortexapps_cli/solutions/workday/data/pied-piper-hierarchy.json" ) @@ -62,7 +62,7 @@ def test_configuration_mapping_fields(): def load_setup_module(): spec = importlib.util.spec_from_file_location( "workday_setup", - "cortexapps_cli/solutions/workday-integration/setup.py", + "cortexapps_cli/solutions/workday/setup.py", ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) @@ -84,7 +84,7 @@ def setup(mod, tmp_path): def test_solution_tag(mod): - assert mod.WorkdayIntegrationSetup.solution_tag == "workday-integration" + assert mod.WorkdayIntegrationSetup.solution_tag == "workday" def test_setup_description(mod): @@ -124,7 +124,7 @@ def test_check_existing_user_accepts_backs_up_and_deletes(setup, tmp_path): resp_del = MagicMock(status_code=204) resp_del.raise_for_status = MagicMock() - backup_dir = tmp_path / "workday-integration" + backup_dir = tmp_path / "workday" with patch("requests.get", return_value=resp_200), \ patch("requests.delete", return_value=resp_del) as mock_delete, \ @@ -136,7 +136,7 @@ def test_check_existing_user_accepts_backs_up_and_deletes(setup, tmp_path): delete_url = mock_delete.call_args.args[0] assert "configurations" in delete_url # plural endpoint - backup_file = tmp_path / ".cortex" / "solutions" / "workday-integration" / "backup-config.json" + backup_file = tmp_path / ".cortex" / "solutions" / "workday" / "backup-config.json" assert backup_file.exists() assert json.loads(backup_file.read_text()) == existing From 9e881f9af2a33c45fad4f78bfd379b25d809e667 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 11:02:31 -0700 Subject: [PATCH 09/20] fix: workday configuration, UI polish, and SETUP_DESCRIPTION wording - Fix configuration.json: add teamId/teamName at root of reportMappingV2 (required by Cortex API for ONE_EMPLOYEE_ONE_TEAM type) - Add CLI-equivalent echo before API call in setup.py for transparency - Reorder README: move Org Hierarchy before Quick Start so Data Model menu shows the hierarchy diagram instead of install commands - Suppress "0 entities imported" line when no entities were imported - Change "post-install setup script" -> "setup script" across all solution SETUP_DESCRIPTIONs and the fallback in solutions.py Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/commands/solutions.py | 6 ++--- .../solutions/github-actions-deploy/setup.py | 2 +- .../solutions/harness-deploy/setup.py | 2 +- cortexapps_cli/solutions/workday/README.md | 24 +++++++++---------- .../solutions/workday/data/configuration.json | 2 ++ cortexapps_cli/solutions/workday/setup.py | 3 ++- tests/test_solutions_postinstall.py | 2 +- tests/test_workday_setup.py | 2 ++ 8 files changed, 24 insertions(+), 19 deletions(-) diff --git a/cortexapps_cli/commands/solutions.py b/cortexapps_cli/commands/solutions.py index c452dd4..1206951 100644 --- a/cortexapps_cli/commands/solutions.py +++ b/cortexapps_cli/commands/solutions.py @@ -223,8 +223,8 @@ def _get_setup_description(solution_tag: str, solutions_dir: str | None = None) """Return the SETUP_DESCRIPTION from a solution's setup.py, or a generic fallback.""" module = _load_setup_module(solution_tag, solutions_dir) if module: - return getattr(module, "SETUP_DESCRIPTION", "This solution includes a post-install setup script.") - return "This solution includes a post-install setup script." + return getattr(module, "SETUP_DESCRIPTION", "This solution includes a setup script.") + return "This solution includes a setup script." def _run_post_install_script(solution_tag: str, solutions_dir: str | None = None, ctx=None, no_prompt: bool = False) -> None: @@ -700,7 +700,7 @@ def _do_import() -> None: if failed_m: typer.echo(failed_m.group(0)) typer.echo(f"\n {total_imported} imported, {total_failed} failed") - else: + elif total_imported > 0: typer.echo(f" {total_imported} entities imported") else: typer.echo(output) diff --git a/cortexapps_cli/solutions/github-actions-deploy/setup.py b/cortexapps_cli/solutions/github-actions-deploy/setup.py index b0e2f36..1dc0863 100644 --- a/cortexapps_cli/solutions/github-actions-deploy/setup.py +++ b/cortexapps_cli/solutions/github-actions-deploy/setup.py @@ -5,7 +5,7 @@ """ SETUP_DESCRIPTION = ( - "This solution includes a post-install setup script that will create a GitHub " + "This solution includes a setup script that will create a GitHub " "repository, seed it with a GitHub workflow that will add a deploy to a Cortex entity, and configure the required secrets." ) import base64 diff --git a/cortexapps_cli/solutions/harness-deploy/setup.py b/cortexapps_cli/solutions/harness-deploy/setup.py index f2d3f20..f23a8f1 100644 --- a/cortexapps_cli/solutions/harness-deploy/setup.py +++ b/cortexapps_cli/solutions/harness-deploy/setup.py @@ -6,7 +6,7 @@ """ SETUP_DESCRIPTION = ( - "This solution includes a post-install setup script that will configure your Harness " + "This solution includes a setup script that will configure your Harness " "integration in Cortex, create the deploy pipeline and cortex_api_key secret in Harness, " "import the Cortex trigger workflow, and optionally fire a test deploy." ) diff --git a/cortexapps_cli/solutions/workday/README.md b/cortexapps_cli/solutions/workday/README.md index 0dc0d1b..47faf6a 100644 --- a/cortexapps_cli/solutions/workday/README.md +++ b/cortexapps_cli/solutions/workday/README.md @@ -7,6 +7,18 @@ description: Configure the Cortex Workday integration with a sample org hierarch Get the Cortex Workday integration running in minutes using a sample org hierarchy from the fictional company Pied Piper (from the TV show *Silicon Valley*). After install, trigger a sync to see employees and teams appear in your catalog — including the full team hierarchy. +## Org Hierarchy + +``` +PP: Pied Piper (Erlich Bachman) +├── PP: Engineering (Richard Hendricks) +│ ├── PP: Platform (Bertram Gilfoyle) +│ │ └── PP: Infrastructure (Nelson Bighetti) +│ └── PP: Frontend (Dinesh Chugtai) +└── PP: Operations (Jared Dunn) + └── PP: People Ops (Monica Hall) +``` + ## What's Included - **Pied Piper org data:** 7 employees across 4 levels of hierarchy (Erlich → Richard → Gilfoyle/Dinesh, Jared → Monica, Bachman → Big Head) @@ -33,18 +45,6 @@ Get the Cortex Workday integration running in minutes using a sample org hierarc 4. Check your team hierarchy to see the Pied Piper org chart. -## Org Hierarchy - -``` -PP: Pied Piper (Erlich Bachman) -├── PP: Engineering (Richard Hendricks) -│ ├── PP: Platform (Bertram Gilfoyle) -│ │ └── PP: Infrastructure (Nelson Bighetti) -│ └── PP: Frontend (Dinesh Chugtai) -└── PP: Operations (Jared Dunn) - └── PP: People Ops (Monica Hall) -``` - ## How It Works The setup script calls the Cortex Workday integration API to configure a report URL pointing at `pied-piper-hierarchy.json` hosted in this repository. Cortex fetches the report and syncs employees and teams into your catalog on the next import run. diff --git a/cortexapps_cli/solutions/workday/data/configuration.json b/cortexapps_cli/solutions/workday/data/configuration.json index cde8c35..d43b4f6 100644 --- a/cortexapps_cli/solutions/workday/data/configuration.json +++ b/cortexapps_cli/solutions/workday/data/configuration.json @@ -7,6 +7,8 @@ "firstName": { "columnName": "firstName" }, "lastName": { "columnName": "lastName" }, "managerEmail": { "columnName": "managersEmail" }, + "teamId": { "columnName": "teamId" }, + "teamName": { "columnName": "teamName" }, "employeeRole": null, "rootTeams": [], "teamListFields": null, diff --git a/cortexapps_cli/solutions/workday/setup.py b/cortexapps_cli/solutions/workday/setup.py index fc8f95d..7a3e2fc 100644 --- a/cortexapps_cli/solutions/workday/setup.py +++ b/cortexapps_cli/solutions/workday/setup.py @@ -5,7 +5,7 @@ """ SETUP_DESCRIPTION = ( - "This solution includes a post-install setup script that will configure " + "This solution includes a setup script that will configure " "the Cortex Workday integration to sync the Pied Piper org hierarchy." ) @@ -80,6 +80,7 @@ def _configure_integration(self) -> None: if self.already_done("configure"): return "Already configured (skipped)" config = json.loads(CONFIG_FILE.read_text()) + print(f" (cortex integrations workday add -f {CONFIG_FILE})") r = requests.post( f"{self._base_url}/api/v1/workday/configuration", headers=self._cortex_headers(), diff --git a/tests/test_solutions_postinstall.py b/tests/test_solutions_postinstall.py index 98deb6e..8c9bc11 100644 --- a/tests/test_solutions_postinstall.py +++ b/tests/test_solutions_postinstall.py @@ -51,5 +51,5 @@ def test_install_prompts_and_runs_post_install_on_yes(): ["-k", "fake", "solutions", "install", "-s", "github-actions-deploy"], input="y\n", ) - assert "This solution includes a post-install setup script" in result.output + assert "This solution includes a setup script" in result.output mock_run.assert_called_once_with("github-actions-deploy", solutions_dir=None, ctx=ANY) diff --git a/tests/test_workday_setup.py b/tests/test_workday_setup.py index 014b104..575c95b 100644 --- a/tests/test_workday_setup.py +++ b/tests/test_workday_setup.py @@ -49,6 +49,8 @@ def test_configuration_mapping_fields(): mapping = config["reportMappingV2"] assert mapping["email"]["columnName"] == "email" assert mapping["managerEmail"]["columnName"] == "managersEmail" + assert mapping["teamId"]["columnName"] == "teamId" + assert mapping["teamName"]["columnName"] == "teamName" ff = mapping["fallbackFields"] assert ff["fieldOnParentNode"]["columnName"] == "teamId" assert ff["fieldOnChildNode"]["columnName"] == "parentTeamId" From adfef29adca9841c39012b340734422a334e333a Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 11:11:26 -0700 Subject: [PATCH 10/20] fix: switch to supervisory org format, add validation step, update import instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace pied-piper-hierarchy.json with pied-piper-supervisory-org.json (uses childHierarchyColumn/parentHierarchyColumn for hierarchy linkage) - Update configuration.json: supervisory org field mapping, point ownershipReportUrl to GitHub Pages for local testing (will switch to raw.githubusercontent.com after merge) - Add _validate_integration() step that calls POST /api/v1/workday/configuration/validate and warns on failure without aborting - Update post_steps() instructions: Catalog → All Entities → Import Entities → Workday → Sync Entities → Next Step - Update tests for new data shape and validation step Co-Authored-By: Claude Sonnet 4.6 --- .../solutions/workday/data/configuration.json | 6 +-- ...y.json => pied-piper-supervisory-org.json} | 21 ++++++--- cortexapps_cli/solutions/workday/setup.py | 18 ++++++- tests/test_workday_setup.py | 47 +++++++++++++------ 4 files changed, 65 insertions(+), 27 deletions(-) rename cortexapps_cli/solutions/workday/data/{pied-piper-hierarchy.json => pied-piper-supervisory-org.json} (77%) diff --git a/cortexapps_cli/solutions/workday/data/configuration.json b/cortexapps_cli/solutions/workday/data/configuration.json index d43b4f6..264aa7c 100644 --- a/cortexapps_cli/solutions/workday/data/configuration.json +++ b/cortexapps_cli/solutions/workday/data/configuration.json @@ -1,6 +1,6 @@ { "username": "ISU_Cortex", - "ownershipReportUrl": "https://raw.githubusercontent.com/cortexapps/cli/main/cortexapps_cli/solutions/workday/data/pied-piper-hierarchy.json", + "ownershipReportUrl": "https://jeff-test-org.github.io/workday-mocks/pied-piper-supervisory-org/index.json", "reportMappingV2": { "email": { "columnName": "email" }, "employeeId": { "columnName": "employeeId" }, @@ -15,8 +15,8 @@ "fallbackFields": { "teamId": { "columnName": "teamId" }, "teamName": { "columnName": "teamName" }, - "fieldOnParentNode": { "columnName": "teamId" }, - "fieldOnChildNode": { "columnName": "parentTeamId" } + "fieldOnParentNode": { "columnName": "childHierarchyColumn" }, + "fieldOnChildNode": { "columnName": "parentHierarchyColumn" } }, "type": "ONE_EMPLOYEE_ONE_TEAM" }, diff --git a/cortexapps_cli/solutions/workday/data/pied-piper-hierarchy.json b/cortexapps_cli/solutions/workday/data/pied-piper-supervisory-org.json similarity index 77% rename from cortexapps_cli/solutions/workday/data/pied-piper-hierarchy.json rename to cortexapps_cli/solutions/workday/data/pied-piper-supervisory-org.json index 3f96ee7..fb851fc 100644 --- a/cortexapps_cli/solutions/workday/data/pied-piper-hierarchy.json +++ b/cortexapps_cli/solutions/workday/data/pied-piper-supervisory-org.json @@ -8,7 +8,8 @@ "managersEmail": "erlich.bachman@piedpiper.com", "teamId": "WORKTEAM-1-000", "teamName": "PP: Pied Piper", - "parentTeamId": "NONE" + "childHierarchyColumn": null, + "parentHierarchyColumn": "SUP-000" }, { "email": "richard.hendricks@piedpiper.com", @@ -18,7 +19,8 @@ "managersEmail": "erlich.bachman@piedpiper.com", "teamId": "WORKTEAM-1-001", "teamName": "PP: Engineering", - "parentTeamId": "WORKTEAM-1-000" + "childHierarchyColumn": "SUP-000", + "parentHierarchyColumn": "SUP-001" }, { "email": "bertram.gilfoyle@piedpiper.com", @@ -28,7 +30,8 @@ "managersEmail": "richard.hendricks@piedpiper.com", "teamId": "WORKTEAM-1-002", "teamName": "PP: Platform", - "parentTeamId": "WORKTEAM-1-001" + "childHierarchyColumn": "SUP-001", + "parentHierarchyColumn": "SUP-002" }, { "email": "dinesh.chugtai@piedpiper.com", @@ -38,7 +41,8 @@ "managersEmail": "richard.hendricks@piedpiper.com", "teamId": "WORKTEAM-1-003", "teamName": "PP: Frontend", - "parentTeamId": "WORKTEAM-1-001" + "childHierarchyColumn": "SUP-001", + "parentHierarchyColumn": null }, { "email": "jared.dunn@piedpiper.com", @@ -48,7 +52,8 @@ "managersEmail": "erlich.bachman@piedpiper.com", "teamId": "WORKTEAM-1-004", "teamName": "PP: Operations", - "parentTeamId": "WORKTEAM-1-000" + "childHierarchyColumn": "SUP-000", + "parentHierarchyColumn": "SUP-004" }, { "email": "monica.hall@piedpiper.com", @@ -58,7 +63,8 @@ "managersEmail": "jared.dunn@piedpiper.com", "teamId": "WORKTEAM-1-005", "teamName": "PP: People Ops", - "parentTeamId": "WORKTEAM-1-004" + "childHierarchyColumn": "SUP-004", + "parentHierarchyColumn": null }, { "email": "nelson.bighetti@piedpiper.com", @@ -68,7 +74,8 @@ "managersEmail": "bertram.gilfoyle@piedpiper.com", "teamId": "WORKTEAM-1-006", "teamName": "PP: Infrastructure", - "parentTeamId": "WORKTEAM-1-002" + "childHierarchyColumn": "SUP-002", + "parentHierarchyColumn": null } ] } diff --git a/cortexapps_cli/solutions/workday/setup.py b/cortexapps_cli/solutions/workday/setup.py index 7a3e2fc..4aa31fd 100644 --- a/cortexapps_cli/solutions/workday/setup.py +++ b/cortexapps_cli/solutions/workday/setup.py @@ -92,16 +92,30 @@ def _configure_integration(self) -> None: ) self.mark_done("configure") + def _validate_integration(self) -> None: + """Validate the Workday integration configuration.""" + print(f" (cortex integrations workday validate)") + r = requests.post( + f"{self._base_url}/api/v1/workday/configuration/validate", + headers=self._cortex_headers(), + ) + if not r.ok: + print(f" ⚠ Validation returned {r.status_code}: {r.text}") + else: + print(f" ✓ Configuration validated successfully") + def steps(self) -> list: return [ ("Check for existing Workday integration", self._check_and_replace_existing), ("Configure Workday integration", self._configure_integration), + ("Validate Workday integration", self._validate_integration), ] def post_steps(self) -> None: print("\n✓ Workday integration configured with the Pied Piper org hierarchy.\n") - print("Next: trigger the import in Cortex:") - print(" Catalog → All Entities → Import Entities\n") + print("Next: trigger a sync in Cortex:") + print(" Catalog → All Entities → Import Entities → Workday") + print(" Click 'Sync Entities', then 'Next Step' to import.\n") print("Then check your team hierarchy to see the Pied Piper org chart.") diff --git a/tests/test_workday_setup.py b/tests/test_workday_setup.py index 575c95b..2747c30 100644 --- a/tests/test_workday_setup.py +++ b/tests/test_workday_setup.py @@ -2,36 +2,33 @@ from pathlib import Path DATA_DIR = Path("cortexapps_cli/solutions/workday/data") -REPORT_URL = ( - "https://raw.githubusercontent.com/cortexapps/cli/main" - "/cortexapps_cli/solutions/workday/data/pied-piper-hierarchy.json" -) +REPORT_URL = "https://jeff-test-org.github.io/workday-mocks/pied-piper-supervisory-org/index.json" def test_hierarchy_json_is_valid(): - data = json.loads((DATA_DIR / "pied-piper-hierarchy.json").read_text()) + data = json.loads((DATA_DIR / "pied-piper-supervisory-org.json").read_text()) assert "Report_Entry" in data assert len(data["Report_Entry"]) == 7 def test_hierarchy_has_root_employee(): - data = json.loads((DATA_DIR / "pied-piper-hierarchy.json").read_text()) + data = json.loads((DATA_DIR / "pied-piper-supervisory-org.json").read_text()) roots = [e for e in data["Report_Entry"] if e["managersEmail"] == e["email"]] assert len(roots) == 1 assert roots[0]["email"] == "erlich.bachman@piedpiper.com" def test_hierarchy_has_root_team(): - data = json.loads((DATA_DIR / "pied-piper-hierarchy.json").read_text()) - roots = [e for e in data["Report_Entry"] if e["parentTeamId"] == "NONE"] + data = json.loads((DATA_DIR / "pied-piper-supervisory-org.json").read_text()) + roots = [e for e in data["Report_Entry"] if e["childHierarchyColumn"] is None] assert len(roots) == 1 assert roots[0]["teamId"] == "WORKTEAM-1-000" def test_hierarchy_required_fields(): - data = json.loads((DATA_DIR / "pied-piper-hierarchy.json").read_text()) + data = json.loads((DATA_DIR / "pied-piper-supervisory-org.json").read_text()) required = {"email", "employeeId", "firstName", "lastName", "managersEmail", - "teamId", "teamName", "parentTeamId"} + "teamId", "teamName", "childHierarchyColumn", "parentHierarchyColumn"} for entry in data["Report_Entry"]: assert required <= entry.keys(), f"Missing fields in entry: {entry}" @@ -52,8 +49,8 @@ def test_configuration_mapping_fields(): assert mapping["teamId"]["columnName"] == "teamId" assert mapping["teamName"]["columnName"] == "teamName" ff = mapping["fallbackFields"] - assert ff["fieldOnParentNode"]["columnName"] == "teamId" - assert ff["fieldOnChildNode"]["columnName"] == "parentTeamId" + assert ff["fieldOnParentNode"]["columnName"] == "childHierarchyColumn" + assert ff["fieldOnChildNode"]["columnName"] == "parentHierarchyColumn" import importlib.util @@ -126,8 +123,6 @@ def test_check_existing_user_accepts_backs_up_and_deletes(setup, tmp_path): resp_del = MagicMock(status_code=204) resp_del.raise_for_status = MagicMock() - backup_dir = tmp_path / "workday" - with patch("requests.get", return_value=resp_200), \ patch("requests.delete", return_value=resp_del) as mock_delete, \ patch("builtins.input", return_value="y"), \ @@ -154,7 +149,7 @@ def test_configure_integration_posts_correct_payload(setup): payload = mock_post.call_args.kwargs["json"] assert payload["reportMappingV2"]["type"] == "ONE_EMPLOYEE_ONE_TEAM" - assert "pied-piper-hierarchy.json" in payload["ownershipReportUrl"] + assert "pied-piper-supervisory-org" in payload["ownershipReportUrl"] def test_configure_integration_raises_on_failure(setup): @@ -171,5 +166,27 @@ def test_configure_integration_idempotent(setup, tmp_path): mock_post.assert_not_called() +def test_validate_integration_success(setup, capsys): + resp = MagicMock(ok=True, status_code=200) + with patch("requests.post", return_value=resp): + setup._validate_integration() + out = capsys.readouterr().out + assert "validated successfully" in out + + +def test_validate_integration_warns_on_failure(setup, capsys): + resp = MagicMock(ok=False, status_code=400, text="bad") + with patch("requests.post", return_value=resp): + setup._validate_integration() # must NOT raise + out = capsys.readouterr().out + assert "Validation returned" in out + + +def test_steps_includes_validate(setup): + steps = setup.steps() + labels = [s[0] for s in steps] + assert "Validate Workday integration" in labels + + def test_main_callable(mod): assert callable(mod.main) From 6a12553767fab2cc6e77afcd285a082d83c2e3db Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 11:15:15 -0700 Subject: [PATCH 11/20] fix: mark_undone after deleting existing config, parse isValid from validate response - Add mark_undone() to SolutionSetup base class so step state can be cleared when the underlying resource is deleted - Call mark_undone("configure") after deleting existing Workday config, so the configure step isn't skipped on the next run - Fix _validate_integration(): validate endpoint always returns HTTP 200; must check configurations[0].isValid in the JSON body and raise on false - Update tests for all three fixes Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/solutions/_lib/setup_base.py | 5 ++++ cortexapps_cli/solutions/workday/setup.py | 13 +++++++--- tests/test_setup_base.py | 16 ++++++++++++ tests/test_workday_setup.py | 28 +++++++++++++++++---- 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/cortexapps_cli/solutions/_lib/setup_base.py b/cortexapps_cli/solutions/_lib/setup_base.py index a74cef3..fcb5788 100644 --- a/cortexapps_cli/solutions/_lib/setup_base.py +++ b/cortexapps_cli/solutions/_lib/setup_base.py @@ -139,6 +139,11 @@ def mark_done(self, key: str) -> None: self._state[key] = True self._save_file() + def mark_undone(self, key: str) -> None: + """Clear a step's completion state (e.g., after the underlying resource is deleted).""" + self._state.pop(key, None) + self._save_file() + @abstractmethod def collect_prompts(self) -> None: """Collect all user inputs upfront before executing steps.""" diff --git a/cortexapps_cli/solutions/workday/setup.py b/cortexapps_cli/solutions/workday/setup.py index 4aa31fd..7ff3c03 100644 --- a/cortexapps_cli/solutions/workday/setup.py +++ b/cortexapps_cli/solutions/workday/setup.py @@ -74,6 +74,7 @@ def _check_and_replace_existing(self) -> None: headers=self._cortex_headers(), ) del_r.raise_for_status() + self.mark_undone("configure") def _configure_integration(self) -> None: """POST the bundled Workday integration configuration.""" @@ -99,10 +100,14 @@ def _validate_integration(self) -> None: f"{self._base_url}/api/v1/workday/configuration/validate", headers=self._cortex_headers(), ) - if not r.ok: - print(f" ⚠ Validation returned {r.status_code}: {r.text}") - else: - print(f" ✓ Configuration validated successfully") + r.raise_for_status() + configs = r.json().get("configurations", []) + if not configs: + raise RuntimeError("Validation returned no results — is the integration configured?") + result = configs[0] + if not result.get("isValid", False): + raise RuntimeError(f"Validation failed: {result.get('message', 'unknown error')}") + print(f" ✓ Configuration validated successfully") def steps(self) -> list: return [ diff --git a/tests/test_setup_base.py b/tests/test_setup_base.py index 60092b3..2fa72fb 100644 --- a/tests/test_setup_base.py +++ b/tests/test_setup_base.py @@ -76,6 +76,22 @@ def test_mark_done_persists_across_instances(tmp_path): assert ConcreteSetup(state_dir=tmp_path).already_done("step1") is True +def test_mark_undone_clears_state(tmp_path): + setup = ConcreteSetup(state_dir=tmp_path) + setup.mark_done("step1") + assert setup.already_done("step1") is True + setup.mark_undone("step1") + assert setup.already_done("step1") is False + # persists across instances + assert ConcreteSetup(state_dir=tmp_path).already_done("step1") is False + + +def test_mark_undone_noop_when_not_set(tmp_path): + setup = ConcreteSetup(state_dir=tmp_path) + setup.mark_undone("nonexistent") # must not raise + assert setup.already_done("nonexistent") is False + + def test_state_file_path(tmp_path): setup = ConcreteSetup(state_dir=tmp_path) assert setup._state_file == tmp_path / "test-solution.json" diff --git a/tests/test_workday_setup.py b/tests/test_workday_setup.py index 2747c30..4cd87c9 100644 --- a/tests/test_workday_setup.py +++ b/tests/test_workday_setup.py @@ -123,6 +123,9 @@ def test_check_existing_user_accepts_backs_up_and_deletes(setup, tmp_path): resp_del = MagicMock(status_code=204) resp_del.raise_for_status = MagicMock() + setup.mark_done("configure") # simulate a prior completed run + assert setup.already_done("configure") + with patch("requests.get", return_value=resp_200), \ patch("requests.delete", return_value=resp_del) as mock_delete, \ patch("builtins.input", return_value="y"), \ @@ -133,6 +136,9 @@ def test_check_existing_user_accepts_backs_up_and_deletes(setup, tmp_path): delete_url = mock_delete.call_args.args[0] assert "configurations" in delete_url # plural endpoint + # configure state must be cleared so the next step doesn't skip + assert not setup.already_done("configure") + backup_file = tmp_path / ".cortex" / "solutions" / "workday" / "backup-config.json" assert backup_file.exists() assert json.loads(backup_file.read_text()) == existing @@ -168,18 +174,30 @@ def test_configure_integration_idempotent(setup, tmp_path): def test_validate_integration_success(setup, capsys): resp = MagicMock(ok=True, status_code=200) + resp.raise_for_status = MagicMock() + resp.json.return_value = {"configurations": [{"isValid": True, "alias": "default"}]} with patch("requests.post", return_value=resp): setup._validate_integration() out = capsys.readouterr().out assert "validated successfully" in out -def test_validate_integration_warns_on_failure(setup, capsys): - resp = MagicMock(ok=False, status_code=400, text="bad") +def test_validate_integration_raises_on_invalid(setup): + resp = MagicMock(ok=True, status_code=200) + resp.raise_for_status = MagicMock() + resp.json.return_value = {"configurations": [{"isValid": False, "message": "404 from URL", "alias": "default"}]} with patch("requests.post", return_value=resp): - setup._validate_integration() # must NOT raise - out = capsys.readouterr().out - assert "Validation returned" in out + with pytest.raises(RuntimeError, match="Validation failed: 404 from URL"): + setup._validate_integration() + + +def test_validate_integration_raises_on_empty_result(setup): + resp = MagicMock(ok=True, status_code=200) + resp.raise_for_status = MagicMock() + resp.json.return_value = {"configurations": []} + with patch("requests.post", return_value=resp): + with pytest.raises(RuntimeError, match="no results"): + setup._validate_integration() def test_steps_includes_validate(setup): From 5a82c0410b50245dfaa4516b17693175d433ae8e Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 11:21:45 -0700 Subject: [PATCH 12/20] fix: clear stale configure state when no integration exists on server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The already_done("configure") cache can go stale if the Workday integration is deleted outside of the setup script. The check step now calls mark_undone("configure") on 404 (no integration on server), ensuring the configure step always runs when there's nothing to skip. Combined with the previous fix (mark_undone after user-confirmed delete), all paths are now covered: - No integration on server (404) → mark_undone → configure runs - Integration found, user deletes → mark_undone → configure runs - Integration found, user keeps → state unchanged → configure skips Also: update README Data Model section with architecture flow diagram showing Workday Report → Cortex Integration → Team Catalog alongside the Pied Piper org hierarchy. Co-Authored-By: Claude Sonnet 4.6 --- cortexapps_cli/solutions/workday/README.md | 29 ++++++++++++++-------- cortexapps_cli/solutions/workday/setup.py | 3 ++- tests/test_workday_setup.py | 4 ++- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/cortexapps_cli/solutions/workday/README.md b/cortexapps_cli/solutions/workday/README.md index 47faf6a..4d5fa97 100644 --- a/cortexapps_cli/solutions/workday/README.md +++ b/cortexapps_cli/solutions/workday/README.md @@ -7,21 +7,28 @@ description: Configure the Cortex Workday integration with a sample org hierarch Get the Cortex Workday integration running in minutes using a sample org hierarchy from the fictional company Pied Piper (from the TV show *Silicon Valley*). After install, trigger a sync to see employees and teams appear in your catalog — including the full team hierarchy. -## Org Hierarchy +## Data Model ``` -PP: Pied Piper (Erlich Bachman) -├── PP: Engineering (Richard Hendricks) -│ ├── PP: Platform (Bertram Gilfoyle) -│ │ └── PP: Infrastructure (Nelson Bighetti) -│ └── PP: Frontend (Dinesh Chugtai) -└── PP: Operations (Jared Dunn) - └── PP: People Ops (Monica Hall) + ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐ + │ Workday Report │────▶│ Cortex Integration │────▶│ Team Catalog │ + │ (JSON / RaaS) │ │ (field mapping) │ │ + Members │ + └──────────────────────┘ └──────────────────────┘ └──────────────────────┘ + + Sample: Pied Piper supervisory org (Silicon Valley) + + PP: Pied Piper (Erlich Bachman) + ├── PP: Engineering (Richard Hendricks) + │ ├── PP: Platform (Bertram Gilfoyle) + │ │ └── PP: Infrastructure (Nelson Bighetti) + │ └── PP: Frontend (Dinesh Chugtai) + └── PP: Operations (Jared Dunn) + └── PP: People Ops (Monica Hall) ``` ## What's Included -- **Pied Piper org data:** 7 employees across 4 levels of hierarchy (Erlich → Richard → Gilfoyle/Dinesh, Jared → Monica, Bachman → Big Head) +- **Pied Piper org data:** 7 employees across 4 levels of hierarchy (Erlich → Richard → Gilfoyle/Dinesh, Jared → Monica, Gilfoyle → Big Head) - **Integration config:** field mapping and report URL pre-configured, pointing at the hosted data - **Setup script:** one-command configuration of the Cortex Workday integration via API @@ -41,13 +48,13 @@ PP: Pied Piper (Erlich Bachman) 3. Trigger the import in Cortex: - **Catalog → All Entities → Import Entities** + **Catalog → All Entities → Import Entities → Workday → Sync Entities → Next Step** 4. Check your team hierarchy to see the Pied Piper org chart. ## How It Works -The setup script calls the Cortex Workday integration API to configure a report URL pointing at `pied-piper-hierarchy.json` hosted in this repository. Cortex fetches the report and syncs employees and teams into your catalog on the next import run. +The setup script calls the Cortex Workday integration API to configure a report URL pointing at the bundled Pied Piper supervisory org data. Cortex fetches the report and syncs employees and teams into your catalog on the next import run. ## Adapting to Real Workday Data diff --git a/cortexapps_cli/solutions/workday/setup.py b/cortexapps_cli/solutions/workday/setup.py index 7ff3c03..ec14d9a 100644 --- a/cortexapps_cli/solutions/workday/setup.py +++ b/cortexapps_cli/solutions/workday/setup.py @@ -54,7 +54,8 @@ def _check_and_replace_existing(self) -> None: headers=self._cortex_headers(), ) if r.status_code == 404: - return # no existing config — proceed + self.mark_undone("configure") # no integration on server — reconfigure regardless of cached state + return r.raise_for_status() if not self.confirm("Existing Workday integration found. Replace it?", default=False): diff --git a/tests/test_workday_setup.py b/tests/test_workday_setup.py index 4cd87c9..0d1469b 100644 --- a/tests/test_workday_setup.py +++ b/tests/test_workday_setup.py @@ -99,9 +99,11 @@ def test_collect_prompts_is_noop(setup): def test_check_existing_no_config_proceeds(setup): resp_404 = MagicMock(status_code=404) resp_404.raise_for_status = MagicMock() + setup.mark_done("configure") # simulate stale cached state with patch("requests.get", return_value=resp_404): - # Should return without prompting or raising setup._check_and_replace_existing() + # stale cache must be cleared so configure step runs + assert not setup.already_done("configure") def test_check_existing_user_declines_exits(setup): From 378a3e3e4edb571f270c9e6bcb36f8aa118d7804 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 11:23:28 -0700 Subject: [PATCH 13/20] fix: use directory URL for ownershipReportUrl (Cortex appends /?format=json) --- cortexapps_cli/solutions/workday/data/configuration.json | 2 +- tests/test_workday_setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cortexapps_cli/solutions/workday/data/configuration.json b/cortexapps_cli/solutions/workday/data/configuration.json index 264aa7c..838c954 100644 --- a/cortexapps_cli/solutions/workday/data/configuration.json +++ b/cortexapps_cli/solutions/workday/data/configuration.json @@ -1,6 +1,6 @@ { "username": "ISU_Cortex", - "ownershipReportUrl": "https://jeff-test-org.github.io/workday-mocks/pied-piper-supervisory-org/index.json", + "ownershipReportUrl": "https://jeff-test-org.github.io/workday-mocks/pied-piper-supervisory-org", "reportMappingV2": { "email": { "columnName": "email" }, "employeeId": { "columnName": "employeeId" }, diff --git a/tests/test_workday_setup.py b/tests/test_workday_setup.py index 0d1469b..1f99268 100644 --- a/tests/test_workday_setup.py +++ b/tests/test_workday_setup.py @@ -2,7 +2,7 @@ from pathlib import Path DATA_DIR = Path("cortexapps_cli/solutions/workday/data") -REPORT_URL = "https://jeff-test-org.github.io/workday-mocks/pied-piper-supervisory-org/index.json" +REPORT_URL = "https://jeff-test-org.github.io/workday-mocks/pied-piper-supervisory-org" def test_hierarchy_json_is_valid(): From 2c526c5c13d7ba4080c32b4533894962f788d512 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 11:28:30 -0700 Subject: [PATCH 14/20] fix: swap fieldOnParentNode/fieldOnChildNode in supervisory org mapping parent[fieldOnParentNode] must equal child[fieldOnChildNode] for Cortex to link teams in a hierarchy. The columns were reversed: fieldOnParentNode = parentHierarchyColumn (parent's own SUP ID) fieldOnChildNode = childHierarchyColumn (child's pointer to parent SUP ID) With the previous (wrong) values, Erlich's childHierarchyColumn (null) never matched any child's parentHierarchyColumn, so only the root team was imported. --- cortexapps_cli/solutions/workday/data/configuration.json | 4 ++-- tests/test_workday_setup.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cortexapps_cli/solutions/workday/data/configuration.json b/cortexapps_cli/solutions/workday/data/configuration.json index 838c954..77d6f2e 100644 --- a/cortexapps_cli/solutions/workday/data/configuration.json +++ b/cortexapps_cli/solutions/workday/data/configuration.json @@ -15,8 +15,8 @@ "fallbackFields": { "teamId": { "columnName": "teamId" }, "teamName": { "columnName": "teamName" }, - "fieldOnParentNode": { "columnName": "childHierarchyColumn" }, - "fieldOnChildNode": { "columnName": "parentHierarchyColumn" } + "fieldOnParentNode": { "columnName": "parentHierarchyColumn" }, + "fieldOnChildNode": { "columnName": "childHierarchyColumn" } }, "type": "ONE_EMPLOYEE_ONE_TEAM" }, diff --git a/tests/test_workday_setup.py b/tests/test_workday_setup.py index 1f99268..1bced25 100644 --- a/tests/test_workday_setup.py +++ b/tests/test_workday_setup.py @@ -49,8 +49,8 @@ def test_configuration_mapping_fields(): assert mapping["teamId"]["columnName"] == "teamId" assert mapping["teamName"]["columnName"] == "teamName" ff = mapping["fallbackFields"] - assert ff["fieldOnParentNode"]["columnName"] == "childHierarchyColumn" - assert ff["fieldOnChildNode"]["columnName"] == "parentHierarchyColumn" + assert ff["fieldOnParentNode"]["columnName"] == "parentHierarchyColumn" + assert ff["fieldOnChildNode"]["columnName"] == "childHierarchyColumn" import importlib.util From 8dee5052d98254f1f123ef3ac7b71a6bcb8bc842 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 12:13:00 -0700 Subject: [PATCH 15/20] fix: restructure Pied Piper data to match cortex-cx supervisory org format - Workteam_Group is now a nested array of objects (not flat fields) - Root team uses parentTeamId: "NONE" (string, not null) matching cortex-cx - Team_Managed only present for managers - Managers_Email field added for reporting chain - configuration.json updated to ONE_EMPLOYEE_MULTIPLE_TEAMS matching cortex-cx - Tests updated for nested Workteam_Group structure Co-Authored-By: Claude Sonnet 4.6 --- .../solutions/workday/data/configuration.json | 35 ++-- .../data/pied-piper-supervisory-org.json | 151 ++++++++++-------- tests/test_workday_setup.py | 65 ++++---- 3 files changed, 142 insertions(+), 109 deletions(-) diff --git a/cortexapps_cli/solutions/workday/data/configuration.json b/cortexapps_cli/solutions/workday/data/configuration.json index 77d6f2e..291ab2e 100644 --- a/cortexapps_cli/solutions/workday/data/configuration.json +++ b/cortexapps_cli/solutions/workday/data/configuration.json @@ -2,23 +2,30 @@ "username": "ISU_Cortex", "ownershipReportUrl": "https://jeff-test-org.github.io/workday-mocks/pied-piper-supervisory-org", "reportMappingV2": { - "email": { "columnName": "email" }, - "employeeId": { "columnName": "employeeId" }, - "firstName": { "columnName": "firstName" }, - "lastName": { "columnName": "lastName" }, - "managerEmail": { "columnName": "managersEmail" }, - "teamId": { "columnName": "teamId" }, - "teamName": { "columnName": "teamName" }, - "employeeRole": null, + "email": { "columnName": "Email" }, + "employeeId": { "columnName": "Employee_ID" }, + "firstName": { "columnName": "First_Name" }, + "lastName": { "columnName": "Last_Name" }, + "managerEmail": null, + "employeeRole": { "columnName": "employeeRole" }, "rootTeams": [], - "teamListFields": null, + "teamListFields": { + "teamListKey": { "columnName": "Workteam_Group" }, + "teamId": { "columnName": "teamName" }, + "teamName": { "columnName": "teamDisplayName" }, + "hierarchy": { + "fieldOnChildNode": { "columnName": "parentTeamId" }, + "fieldOnParentNode": { "columnName": "teamName", "isList": false } + }, + "teamEmployeeManages": { "columnName": "Team_Managed" } + }, "fallbackFields": { - "teamId": { "columnName": "teamId" }, - "teamName": { "columnName": "teamName" }, - "fieldOnParentNode": { "columnName": "parentHierarchyColumn" }, - "fieldOnChildNode": { "columnName": "childHierarchyColumn" } + "teamId": null, + "teamName": null, + "fieldOnParentNode": null, + "fieldOnChildNode": null }, - "type": "ONE_EMPLOYEE_ONE_TEAM" + "type": "ONE_EMPLOYEE_MULTIPLE_TEAMS" }, "password": "asdf" } diff --git a/cortexapps_cli/solutions/workday/data/pied-piper-supervisory-org.json b/cortexapps_cli/solutions/workday/data/pied-piper-supervisory-org.json index fb851fc..0242ce8 100644 --- a/cortexapps_cli/solutions/workday/data/pied-piper-supervisory-org.json +++ b/cortexapps_cli/solutions/workday/data/pied-piper-supervisory-org.json @@ -1,81 +1,106 @@ { "Report_Entry": [ { - "email": "erlich.bachman@piedpiper.com", - "employeeId": "100000", - "firstName": "Erlich", - "lastName": "Bachman", - "managersEmail": "erlich.bachman@piedpiper.com", - "teamId": "WORKTEAM-1-000", - "teamName": "PP: Pied Piper", - "childHierarchyColumn": null, - "parentHierarchyColumn": "SUP-000" + "Email": "erlich.bachman@piedpiper.com", + "Employee_ID": "PP-100000", + "First_Name": "Erlich", + "Last_Name": "Bachman", + "Managers_Email": "erlich.bachman@piedpiper.com", + "Workteam_Group": [ + { + "teamName": "PP: Pied Piper", + "teamDisplayName": "PP: Pied Piper", + "parentTeamId": "NONE", + "Team_Managed": "PP: Pied Piper" + } + ] }, { - "email": "richard.hendricks@piedpiper.com", - "employeeId": "100001", - "firstName": "Richard", - "lastName": "Hendricks", - "managersEmail": "erlich.bachman@piedpiper.com", - "teamId": "WORKTEAM-1-001", - "teamName": "PP: Engineering", - "childHierarchyColumn": "SUP-000", - "parentHierarchyColumn": "SUP-001" + "Email": "richard.hendricks@piedpiper.com", + "Employee_ID": "PP-100001", + "First_Name": "Richard", + "Last_Name": "Hendricks", + "Managers_Email": "erlich.bachman@piedpiper.com", + "Workteam_Group": [ + { + "teamName": "PP: Engineering", + "teamDisplayName": "PP: Engineering", + "parentTeamId": "PP: Pied Piper", + "Team_Managed": "PP: Engineering" + } + ] }, { - "email": "bertram.gilfoyle@piedpiper.com", - "employeeId": "100002", - "firstName": "Bertram", - "lastName": "Gilfoyle", - "managersEmail": "richard.hendricks@piedpiper.com", - "teamId": "WORKTEAM-1-002", - "teamName": "PP: Platform", - "childHierarchyColumn": "SUP-001", - "parentHierarchyColumn": "SUP-002" + "Email": "bertram.gilfoyle@piedpiper.com", + "Employee_ID": "PP-100002", + "First_Name": "Bertram", + "Last_Name": "Gilfoyle", + "Managers_Email": "richard.hendricks@piedpiper.com", + "Workteam_Group": [ + { + "teamName": "PP: Platform", + "teamDisplayName": "PP: Platform", + "parentTeamId": "PP: Engineering", + "Team_Managed": "PP: Platform" + } + ] }, { - "email": "dinesh.chugtai@piedpiper.com", - "employeeId": "100003", - "firstName": "Dinesh", - "lastName": "Chugtai", - "managersEmail": "richard.hendricks@piedpiper.com", - "teamId": "WORKTEAM-1-003", - "teamName": "PP: Frontend", - "childHierarchyColumn": "SUP-001", - "parentHierarchyColumn": null + "Email": "dinesh.chugtai@piedpiper.com", + "Employee_ID": "PP-100003", + "First_Name": "Dinesh", + "Last_Name": "Chugtai", + "Managers_Email": "richard.hendricks@piedpiper.com", + "Workteam_Group": [ + { + "teamName": "PP: Frontend", + "teamDisplayName": "PP: Frontend", + "parentTeamId": "PP: Engineering" + } + ] }, { - "email": "jared.dunn@piedpiper.com", - "employeeId": "100004", - "firstName": "Jared", - "lastName": "Dunn", - "managersEmail": "erlich.bachman@piedpiper.com", - "teamId": "WORKTEAM-1-004", - "teamName": "PP: Operations", - "childHierarchyColumn": "SUP-000", - "parentHierarchyColumn": "SUP-004" + "Email": "jared.dunn@piedpiper.com", + "Employee_ID": "PP-100004", + "First_Name": "Jared", + "Last_Name": "Dunn", + "Managers_Email": "erlich.bachman@piedpiper.com", + "Workteam_Group": [ + { + "teamName": "PP: Operations", + "teamDisplayName": "PP: Operations", + "parentTeamId": "PP: Pied Piper", + "Team_Managed": "PP: Operations" + } + ] }, { - "email": "monica.hall@piedpiper.com", - "employeeId": "100005", - "firstName": "Monica", - "lastName": "Hall", - "managersEmail": "jared.dunn@piedpiper.com", - "teamId": "WORKTEAM-1-005", - "teamName": "PP: People Ops", - "childHierarchyColumn": "SUP-004", - "parentHierarchyColumn": null + "Email": "monica.hall@piedpiper.com", + "Employee_ID": "PP-100005", + "First_Name": "Monica", + "Last_Name": "Hall", + "Managers_Email": "jared.dunn@piedpiper.com", + "Workteam_Group": [ + { + "teamName": "PP: People Ops", + "teamDisplayName": "PP: People Ops", + "parentTeamId": "PP: Operations" + } + ] }, { - "email": "nelson.bighetti@piedpiper.com", - "employeeId": "100006", - "firstName": "Nelson", - "lastName": "Bighetti", - "managersEmail": "bertram.gilfoyle@piedpiper.com", - "teamId": "WORKTEAM-1-006", - "teamName": "PP: Infrastructure", - "childHierarchyColumn": "SUP-002", - "parentHierarchyColumn": null + "Email": "nelson.bighetti@piedpiper.com", + "Employee_ID": "PP-100006", + "First_Name": "Nelson", + "Last_Name": "Bighetti", + "Managers_Email": "bertram.gilfoyle@piedpiper.com", + "Workteam_Group": [ + { + "teamName": "PP: Infrastructure", + "teamDisplayName": "PP: Infrastructure", + "parentTeamId": "PP: Platform" + } + ] } ] } diff --git a/tests/test_workday_setup.py b/tests/test_workday_setup.py index 1bced25..56fd254 100644 --- a/tests/test_workday_setup.py +++ b/tests/test_workday_setup.py @@ -11,32 +11,40 @@ def test_hierarchy_json_is_valid(): assert len(data["Report_Entry"]) == 7 -def test_hierarchy_has_root_employee(): +def test_hierarchy_has_root_team(): data = json.loads((DATA_DIR / "pied-piper-supervisory-org.json").read_text()) - roots = [e for e in data["Report_Entry"] if e["managersEmail"] == e["email"]] - assert len(roots) == 1 - assert roots[0]["email"] == "erlich.bachman@piedpiper.com" + all_teams = [team for entry in data["Report_Entry"] for team in entry["Workteam_Group"]] + root_teams = {team["teamName"] for team in all_teams if team["parentTeamId"] == "NONE"} + assert len(root_teams) == 1 + assert "PP: Pied Piper" in root_teams -def test_hierarchy_has_root_team(): +def test_hierarchy_parent_child_links(): data = json.loads((DATA_DIR / "pied-piper-supervisory-org.json").read_text()) - roots = [e for e in data["Report_Entry"] if e["childHierarchyColumn"] is None] - assert len(roots) == 1 - assert roots[0]["teamId"] == "WORKTEAM-1-000" + all_teams = [team for entry in data["Report_Entry"] for team in entry["Workteam_Group"]] + team_names = {team["teamName"] for team in all_teams} + for team in all_teams: + if team["parentTeamId"] != "NONE": + assert team["parentTeamId"] in team_names, ( + f"{team['teamName']}.parentTeamId={team['parentTeamId']!r} not found in any teamName" + ) def test_hierarchy_required_fields(): data = json.loads((DATA_DIR / "pied-piper-supervisory-org.json").read_text()) - required = {"email", "employeeId", "firstName", "lastName", "managersEmail", - "teamId", "teamName", "childHierarchyColumn", "parentHierarchyColumn"} + required_top = {"Email", "Employee_ID", "First_Name", "Last_Name", "Workteam_Group"} + required_team = {"teamName", "teamDisplayName", "parentTeamId"} for entry in data["Report_Entry"]: - assert required <= entry.keys(), f"Missing fields in entry: {entry}" + assert required_top <= entry.keys(), f"Missing top-level fields in entry: {entry}" + assert isinstance(entry["Workteam_Group"], list), f"Workteam_Group must be a list in: {entry}" + for team in entry["Workteam_Group"]: + assert required_team <= team.keys(), f"Missing team fields in: {team}" def test_configuration_json_is_valid(): config = json.loads((DATA_DIR / "configuration.json").read_text()) assert config["ownershipReportUrl"] == REPORT_URL - assert config["reportMappingV2"]["type"] == "ONE_EMPLOYEE_ONE_TEAM" + assert config["reportMappingV2"]["type"] == "ONE_EMPLOYEE_MULTIPLE_TEAMS" assert "password" in config assert "username" in config @@ -44,13 +52,13 @@ def test_configuration_json_is_valid(): def test_configuration_mapping_fields(): config = json.loads((DATA_DIR / "configuration.json").read_text()) mapping = config["reportMappingV2"] - assert mapping["email"]["columnName"] == "email" - assert mapping["managerEmail"]["columnName"] == "managersEmail" - assert mapping["teamId"]["columnName"] == "teamId" - assert mapping["teamName"]["columnName"] == "teamName" - ff = mapping["fallbackFields"] - assert ff["fieldOnParentNode"]["columnName"] == "parentHierarchyColumn" - assert ff["fieldOnChildNode"]["columnName"] == "childHierarchyColumn" + assert mapping["email"]["columnName"] == "Email" + assert mapping["employeeId"]["columnName"] == "Employee_ID" + tlf = mapping["teamListFields"] + assert tlf["teamListKey"]["columnName"] == "Workteam_Group" + assert tlf["teamId"]["columnName"] == "teamName" + assert tlf["hierarchy"]["fieldOnParentNode"]["columnName"] == "teamName" + assert tlf["hierarchy"]["fieldOnChildNode"]["columnName"] == "parentTeamId" import importlib.util @@ -91,9 +99,8 @@ def test_setup_description(mod): def test_collect_prompts_is_noop(setup): - # collect_prompts() must not raise and must not call input() with patch("builtins.input", side_effect=AssertionError("should not prompt")): - setup.collect_prompts() # no exception = pass + setup.collect_prompts() def test_check_existing_no_config_proceeds(setup): @@ -102,7 +109,6 @@ def test_check_existing_no_config_proceeds(setup): setup.mark_done("configure") # simulate stale cached state with patch("requests.get", return_value=resp_404): setup._check_and_replace_existing() - # stale cache must be cleared so configure step runs assert not setup.already_done("configure") @@ -125,7 +131,7 @@ def test_check_existing_user_accepts_backs_up_and_deletes(setup, tmp_path): resp_del = MagicMock(status_code=204) resp_del.raise_for_status = MagicMock() - setup.mark_done("configure") # simulate a prior completed run + setup.mark_done("configure") assert setup.already_done("configure") with patch("requests.get", return_value=resp_200), \ @@ -135,10 +141,7 @@ def test_check_existing_user_accepts_backs_up_and_deletes(setup, tmp_path): setup._check_and_replace_existing() mock_delete.assert_called_once() - delete_url = mock_delete.call_args.args[0] - assert "configurations" in delete_url # plural endpoint - - # configure state must be cleared so the next step doesn't skip + assert "configurations" in mock_delete.call_args.args[0] assert not setup.already_done("configure") backup_file = tmp_path / ".cortex" / "solutions" / "workday" / "backup-config.json" @@ -156,7 +159,7 @@ def test_configure_integration_posts_correct_payload(setup): assert url.endswith("/api/v1/workday/configuration") payload = mock_post.call_args.kwargs["json"] - assert payload["reportMappingV2"]["type"] == "ONE_EMPLOYEE_ONE_TEAM" + assert payload["reportMappingV2"]["type"] == "ONE_EMPLOYEE_MULTIPLE_TEAMS" assert "pied-piper-supervisory-org" in payload["ownershipReportUrl"] @@ -180,8 +183,7 @@ def test_validate_integration_success(setup, capsys): resp.json.return_value = {"configurations": [{"isValid": True, "alias": "default"}]} with patch("requests.post", return_value=resp): setup._validate_integration() - out = capsys.readouterr().out - assert "validated successfully" in out + assert "validated successfully" in capsys.readouterr().out def test_validate_integration_raises_on_invalid(setup): @@ -203,8 +205,7 @@ def test_validate_integration_raises_on_empty_result(setup): def test_steps_includes_validate(setup): - steps = setup.steps() - labels = [s[0] for s in steps] + labels = [s[0] for s in setup.steps()] assert "Validate Workday integration" in labels From a93bd93c739222c260350412fadf1a24aaf461b4 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 13:17:02 -0700 Subject: [PATCH 16/20] fix: add employeeRole field to Pied Piper report entries --- .../solutions/workday/data/pied-piper-supervisory-org.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cortexapps_cli/solutions/workday/data/pied-piper-supervisory-org.json b/cortexapps_cli/solutions/workday/data/pied-piper-supervisory-org.json index 0242ce8..9ae5bfd 100644 --- a/cortexapps_cli/solutions/workday/data/pied-piper-supervisory-org.json +++ b/cortexapps_cli/solutions/workday/data/pied-piper-supervisory-org.json @@ -6,6 +6,7 @@ "First_Name": "Erlich", "Last_Name": "Bachman", "Managers_Email": "erlich.bachman@piedpiper.com", + "employeeRole": "CEO", "Workteam_Group": [ { "teamName": "PP: Pied Piper", @@ -21,6 +22,7 @@ "First_Name": "Richard", "Last_Name": "Hendricks", "Managers_Email": "erlich.bachman@piedpiper.com", + "employeeRole": "CTO", "Workteam_Group": [ { "teamName": "PP: Engineering", @@ -36,6 +38,7 @@ "First_Name": "Bertram", "Last_Name": "Gilfoyle", "Managers_Email": "richard.hendricks@piedpiper.com", + "employeeRole": "Staff Engineer", "Workteam_Group": [ { "teamName": "PP: Platform", @@ -51,6 +54,7 @@ "First_Name": "Dinesh", "Last_Name": "Chugtai", "Managers_Email": "richard.hendricks@piedpiper.com", + "employeeRole": "Senior Engineer", "Workteam_Group": [ { "teamName": "PP: Frontend", @@ -65,6 +69,7 @@ "First_Name": "Jared", "Last_Name": "Dunn", "Managers_Email": "erlich.bachman@piedpiper.com", + "employeeRole": "COO", "Workteam_Group": [ { "teamName": "PP: Operations", @@ -80,6 +85,7 @@ "First_Name": "Monica", "Last_Name": "Hall", "Managers_Email": "jared.dunn@piedpiper.com", + "employeeRole": "VP People", "Workteam_Group": [ { "teamName": "PP: People Ops", @@ -94,6 +100,7 @@ "First_Name": "Nelson", "Last_Name": "Bighetti", "Managers_Email": "bertram.gilfoyle@piedpiper.com", + "employeeRole": "Engineer", "Workteam_Group": [ { "teamName": "PP: Infrastructure", From ef2bff756651afed4d94414464edcf4d416d6f7d Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 13:41:12 -0700 Subject: [PATCH 17/20] fix: add After Installing, Report Format, and Field Mapping sections to README --- cortexapps_cli/solutions/workday/README.md | 87 ++++++++++++++++++++-- 1 file changed, 80 insertions(+), 7 deletions(-) diff --git a/cortexapps_cli/solutions/workday/README.md b/cortexapps_cli/solutions/workday/README.md index 4d5fa97..f538574 100644 --- a/cortexapps_cli/solutions/workday/README.md +++ b/cortexapps_cli/solutions/workday/README.md @@ -26,6 +26,26 @@ Get the Cortex Workday integration running in minutes using a sample org hierarc └── PP: People Ops (Monica Hall) ``` +## After Installing + +Trigger the sync in Cortex to import the Pied Piper org hierarchy: + +**Catalog → All Entities → Import Entities → Workday → Sync Entities → Next Step** + +You should see 7 teams appear in your catalog with the full hierarchy: + +``` +PP: Pied Piper (Erlich Bachman) +├── PP: Engineering (Richard Hendricks) +│ ├── PP: Platform (Bertram Gilfoyle) +│ │ └── PP: Infrastructure (Nelson Bighetti) +│ └── PP: Frontend (Dinesh Chugtai) +└── PP: Operations (Jared Dunn) + └── PP: People Ops (Monica Hall) +``` + +Workday reports can be customized to match your org structure. The configuration defines which report columns map to employee fields, team identity, and the parent-child hierarchy — so the integration works with any supervisory org report that follows the same shape. + ## What's Included - **Pied Piper org data:** 7 employees across 4 levels of hierarchy (Erlich → Richard → Gilfoyle/Dinesh, Jared → Monica, Gilfoyle → Big Head) @@ -56,13 +76,66 @@ Get the Cortex Workday integration running in minutes using a sample org hierarc The setup script calls the Cortex Workday integration API to configure a report URL pointing at the bundled Pied Piper supervisory org data. Cortex fetches the report and syncs employees and teams into your catalog on the next import run. -## Adapting to Real Workday Data +## Report Format + +Each entry in the Workday report represents one employee. The `Workteam_Group` array lists every team the employee belongs to, with hierarchy encoded as a `parentTeamId` pointing to the parent team's `teamName`. Root teams use `"parentTeamId": "NONE"`. Managers have a `Team_Managed` field matching their team's `teamName`. + +```json +{ + "Report_Entry": [ + { + "Email": "erlich.bachman@piedpiper.com", + "Employee_ID": "PP-100000", + "First_Name": "Erlich", + "Last_Name": "Bachman", + "Managers_Email": "erlich.bachman@piedpiper.com", + "employeeRole": "CEO", + "Workteam_Group": [ + { + "teamName": "PP: Pied Piper", + "teamDisplayName": "PP: Pied Piper", + "parentTeamId": "NONE", + "Team_Managed": "PP: Pied Piper" + } + ] + }, + ... + ] +} +``` -To point the integration at a real Workday RaaS report: +## Field Mapping + +The `data/configuration.json` file tells Cortex how to interpret the report columns. It maps employee identity fields, defines the team list array, and specifies which fields encode the parent-child hierarchy: + +```json +{ + "username": "ISU_Cortex", + "password": "", + "ownershipReportUrl": "", + "reportMappingV2": { + "email": { "columnName": "Email" }, + "employeeId": { "columnName": "Employee_ID" }, + "firstName": { "columnName": "First_Name" }, + "lastName": { "columnName": "Last_Name" }, + "employeeRole":{ "columnName": "employeeRole" }, + "managerEmail": null, + "rootTeams": [], + "teamListFields": { + "teamListKey": { "columnName": "Workteam_Group" }, + "teamId": { "columnName": "teamName" }, + "teamName": { "columnName": "teamDisplayName" }, + "hierarchy": { + "fieldOnParentNode": { "columnName": "teamName", "isList": false }, + "fieldOnChildNode": { "columnName": "parentTeamId" } + }, + "teamEmployeeManages": { "columnName": "Team_Managed" } + }, + "type": "ONE_EMPLOYEE_MULTIPLE_TEAMS" + } +} +``` -1. Go to **Settings → Integrations → Workday** in the Cortex UI -2. Update the **Report URL** to your Workday RaaS endpoint -3. Set your real **username** and **password** -4. Trigger a new import +## Adapting to Real Workday Data -The field mapping (`reportMappingV2`) in `data/configuration.json` matches the standard Cortex Workday report format and works unchanged for real Workday data that uses the same column names. +To connect your own Workday report, go to **Settings → Integrations → Workday** in the Cortex UI and update the Report URL, username, and password. The field mapping in `data/configuration.json` works unchanged for any Workday supervisory org report that uses the same column names. From c2d55d043742901faed3e7ec75c5338fc07c8bdc Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 13:46:57 -0700 Subject: [PATCH 18/20] chore: move Report Format and Field Mapping into Data Model section --- cortexapps_cli/solutions/workday/README.md | 104 ++++++++++----------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/cortexapps_cli/solutions/workday/README.md b/cortexapps_cli/solutions/workday/README.md index f538574..4151870 100644 --- a/cortexapps_cli/solutions/workday/README.md +++ b/cortexapps_cli/solutions/workday/README.md @@ -26,57 +26,7 @@ Get the Cortex Workday integration running in minutes using a sample org hierarc └── PP: People Ops (Monica Hall) ``` -## After Installing - -Trigger the sync in Cortex to import the Pied Piper org hierarchy: - -**Catalog → All Entities → Import Entities → Workday → Sync Entities → Next Step** - -You should see 7 teams appear in your catalog with the full hierarchy: - -``` -PP: Pied Piper (Erlich Bachman) -├── PP: Engineering (Richard Hendricks) -│ ├── PP: Platform (Bertram Gilfoyle) -│ │ └── PP: Infrastructure (Nelson Bighetti) -│ └── PP: Frontend (Dinesh Chugtai) -└── PP: Operations (Jared Dunn) - └── PP: People Ops (Monica Hall) -``` - -Workday reports can be customized to match your org structure. The configuration defines which report columns map to employee fields, team identity, and the parent-child hierarchy — so the integration works with any supervisory org report that follows the same shape. - -## What's Included - -- **Pied Piper org data:** 7 employees across 4 levels of hierarchy (Erlich → Richard → Gilfoyle/Dinesh, Jared → Monica, Gilfoyle → Big Head) -- **Integration config:** field mapping and report URL pre-configured, pointing at the hosted data -- **Setup script:** one-command configuration of the Cortex Workday integration via API - -## Quick Start - -1. Install the solution: - - ``` - cortex solutions install -s workday - ``` - -2. Follow the post-install setup prompts, or run later: - - ``` - cortex solutions post-install -s workday - ``` - -3. Trigger the import in Cortex: - - **Catalog → All Entities → Import Entities → Workday → Sync Entities → Next Step** - -4. Check your team hierarchy to see the Pied Piper org chart. - -## How It Works - -The setup script calls the Cortex Workday integration API to configure a report URL pointing at the bundled Pied Piper supervisory org data. Cortex fetches the report and syncs employees and teams into your catalog on the next import run. - -## Report Format +### Report Format Each entry in the Workday report represents one employee. The `Workteam_Group` array lists every team the employee belongs to, with hierarchy encoded as a `parentTeamId` pointing to the parent team's `teamName`. Root teams use `"parentTeamId": "NONE"`. Managers have a `Team_Managed` field matching their team's `teamName`. @@ -104,7 +54,7 @@ Each entry in the Workday report represents one employee. The `Workteam_Group` a } ``` -## Field Mapping +### Field Mapping The `data/configuration.json` file tells Cortex how to interpret the report columns. It maps employee identity fields, defines the team list array, and specifies which fields encode the parent-child hierarchy: @@ -136,6 +86,56 @@ The `data/configuration.json` file tells Cortex how to interpret the report colu } ``` +## After Installing + +Trigger the sync in Cortex to import the Pied Piper org hierarchy: + +**Catalog → All Entities → Import Entities → Workday → Sync Entities → Next Step** + +You should see 7 teams appear in your catalog with the full hierarchy: + +``` +PP: Pied Piper (Erlich Bachman) +├── PP: Engineering (Richard Hendricks) +│ ├── PP: Platform (Bertram Gilfoyle) +│ │ └── PP: Infrastructure (Nelson Bighetti) +│ └── PP: Frontend (Dinesh Chugtai) +└── PP: Operations (Jared Dunn) + └── PP: People Ops (Monica Hall) +``` + +Workday reports can be customized to match your org structure. The configuration defines which report columns map to employee fields, team identity, and the parent-child hierarchy — so the integration works with any supervisory org report that follows the same shape. + +## What's Included + +- **Pied Piper org data:** 7 employees across 4 levels of hierarchy (Erlich → Richard → Gilfoyle/Dinesh, Jared → Monica, Gilfoyle → Big Head) +- **Integration config:** field mapping and report URL pre-configured, pointing at the hosted data +- **Setup script:** one-command configuration of the Cortex Workday integration via API + +## Quick Start + +1. Install the solution: + + ``` + cortex solutions install -s workday + ``` + +2. Follow the post-install setup prompts, or run later: + + ``` + cortex solutions post-install -s workday + ``` + +3. Trigger the import in Cortex: + + **Catalog → All Entities → Import Entities → Workday → Sync Entities → Next Step** + +4. Check your team hierarchy to see the Pied Piper org chart. + +## How It Works + +The setup script calls the Cortex Workday integration API to configure a report URL pointing at the bundled Pied Piper supervisory org data. Cortex fetches the report and syncs employees and teams into your catalog on the next import run. + ## Adapting to Real Workday Data To connect your own Workday report, go to **Settings → Integrations → Workday** in the Cortex UI and update the Report URL, username, and password. The field mapping in `data/configuration.json` works unchanged for any Workday supervisory org report that uses the same column names. From 59c5bfcfb2587729fa8618bf32319e7e18df36fc Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 13:48:53 -0700 Subject: [PATCH 19/20] chore: consolidate Data Model, Report Format, and Field Mapping into single code block --- cortexapps_cli/solutions/workday/README.md | 108 ++++++++++----------- 1 file changed, 52 insertions(+), 56 deletions(-) diff --git a/cortexapps_cli/solutions/workday/README.md b/cortexapps_cli/solutions/workday/README.md index 4151870..519a4b6 100644 --- a/cortexapps_cli/solutions/workday/README.md +++ b/cortexapps_cli/solutions/workday/README.md @@ -24,66 +24,62 @@ Get the Cortex Workday integration running in minutes using a sample org hierarc │ └── PP: Frontend (Dinesh Chugtai) └── PP: Operations (Jared Dunn) └── PP: People Ops (Monica Hall) -``` -### Report Format - -Each entry in the Workday report represents one employee. The `Workteam_Group` array lists every team the employee belongs to, with hierarchy encoded as a `parentTeamId` pointing to the parent team's `teamName`. Root teams use `"parentTeamId": "NONE"`. Managers have a `Team_Managed` field matching their team's `teamName`. - -```json -{ - "Report_Entry": [ - { - "Email": "erlich.bachman@piedpiper.com", - "Employee_ID": "PP-100000", - "First_Name": "Erlich", - "Last_Name": "Bachman", - "Managers_Email": "erlich.bachman@piedpiper.com", - "employeeRole": "CEO", - "Workteam_Group": [ - { - "teamName": "PP: Pied Piper", - "teamDisplayName": "PP: Pied Piper", - "parentTeamId": "NONE", - "Team_Managed": "PP: Pied Piper" - } - ] - }, - ... - ] -} -``` + Report Format + ───────────── + Each entry represents one employee. Workteam_Group lists the employee's teams. + Hierarchy is encoded as parentTeamId → teamName. Root teams use "NONE". + Managers have Team_Managed set to their team's teamName. + + { + "Report_Entry": [ + { + "Email": "erlich.bachman@piedpiper.com", + "Employee_ID": "PP-100000", + "First_Name": "Erlich", + "Last_Name": "Bachman", + "Managers_Email": "erlich.bachman@piedpiper.com", + "employeeRole": "CEO", + "Workteam_Group": [ + { + "teamName": "PP: Pied Piper", + "teamDisplayName": "PP: Pied Piper", + "parentTeamId": "NONE", + "Team_Managed": "PP: Pied Piper" + } + ] + }, + ... + ] + } -### Field Mapping - -The `data/configuration.json` file tells Cortex how to interpret the report columns. It maps employee identity fields, defines the team list array, and specifies which fields encode the parent-child hierarchy: - -```json -{ - "username": "ISU_Cortex", - "password": "", - "ownershipReportUrl": "", - "reportMappingV2": { - "email": { "columnName": "Email" }, - "employeeId": { "columnName": "Employee_ID" }, - "firstName": { "columnName": "First_Name" }, - "lastName": { "columnName": "Last_Name" }, - "employeeRole":{ "columnName": "employeeRole" }, - "managerEmail": null, - "rootTeams": [], - "teamListFields": { - "teamListKey": { "columnName": "Workteam_Group" }, - "teamId": { "columnName": "teamName" }, - "teamName": { "columnName": "teamDisplayName" }, - "hierarchy": { - "fieldOnParentNode": { "columnName": "teamName", "isList": false }, - "fieldOnChildNode": { "columnName": "parentTeamId" } + Field Mapping (data/configuration.json) + ──────────────────────────────────────── + Maps report columns to employee fields, team identity, and hierarchy. + Workday reports can be customized — the configuration defines the field + attributes and the hierarchy, so it works with any supervisory org report. + + { + "ownershipReportUrl": "", + "reportMappingV2": { + "email": { "columnName": "Email" }, + "employeeId": { "columnName": "Employee_ID" }, + "firstName": { "columnName": "First_Name" }, + "lastName": { "columnName": "Last_Name" }, + "employeeRole": { "columnName": "employeeRole" }, + "teamListFields": { + "teamListKey": { "columnName": "Workteam_Group" }, + "teamId": { "columnName": "teamName" }, + "teamName": { "columnName": "teamDisplayName" }, + "hierarchy": { + "fieldOnParentNode": { "columnName": "teamName" }, + "fieldOnChildNode": { "columnName": "parentTeamId" } + }, + "teamEmployeeManages": { "columnName": "Team_Managed" } }, - "teamEmployeeManages": { "columnName": "Team_Managed" } - }, - "type": "ONE_EMPLOYEE_MULTIPLE_TEAMS" + "type": "ONE_EMPLOYEE_MULTIPLE_TEAMS" + } } -} ``` ## After Installing From 3133849bbed38f00029128dafc6947c6f120fb84 Mon Sep 17 00:00:00 2001 From: Jeff Schnitter Date: Fri, 14 Aug 2026 13:56:04 -0700 Subject: [PATCH 20/20] fix: add prerequisite step to enable Auto Import Workday teams --- cortexapps_cli/solutions/workday/setup.py | 8 ++++++++ tests/test_workday_setup.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/cortexapps_cli/solutions/workday/setup.py b/cortexapps_cli/solutions/workday/setup.py index ec14d9a..3262d6f 100644 --- a/cortexapps_cli/solutions/workday/setup.py +++ b/cortexapps_cli/solutions/workday/setup.py @@ -110,8 +110,16 @@ def _validate_integration(self) -> None: raise RuntimeError(f"Validation failed: {result.get('message', 'unknown error')}") print(f" ✓ Configuration validated successfully") + def _enable_auto_import(self) -> None: + """Prompt user to enable Auto Import Workday Teams in Cortex settings.""" + print(" In Cortex: Settings → Entities → Teams → Enable 'Auto import Workday teams'") + if not self.confirm(" Is 'Auto import Workday teams' enabled?", default=False): + print("Please enable it before continuing.") + raise SystemExit(0) + def steps(self) -> list: return [ + ("Enable Auto Import Workday teams", self._enable_auto_import), ("Check for existing Workday integration", self._check_and_replace_existing), ("Configure Workday integration", self._configure_integration), ("Validate Workday integration", self._validate_integration), diff --git a/tests/test_workday_setup.py b/tests/test_workday_setup.py index 56fd254..b25868c 100644 --- a/tests/test_workday_setup.py +++ b/tests/test_workday_setup.py @@ -204,6 +204,24 @@ def test_validate_integration_raises_on_empty_result(setup): setup._validate_integration() +def test_steps_includes_auto_import(setup): + labels = [s[0] for s in setup.steps()] + assert "Enable Auto Import Workday teams" in labels + assert labels.index("Enable Auto Import Workday teams") == 0 + + +def test_auto_import_exits_when_user_declines(setup): + with patch("builtins.input", return_value="n"): + with pytest.raises(SystemExit) as exc_info: + setup._enable_auto_import() + assert exc_info.value.code == 0 + + +def test_auto_import_proceeds_when_user_confirms(setup): + with patch("builtins.input", return_value="y"): + setup._enable_auto_import() # should not raise + + def test_steps_includes_validate(setup): labels = [s[0] for s in setup.steps()] assert "Validate Workday integration" in labels