diff --git a/plugins/github-copilot-modernization/.mcp.json b/plugins/github-copilot-modernization/.mcp.json index 018e5d6..5661da1 100644 --- a/plugins/github-copilot-modernization/.mcp.json +++ b/plugins/github-copilot-modernization/.mcp.json @@ -1,11 +1,11 @@ { "mcpServers": { "appmod-mcp-server": { - "type": "local", + "type": "stdio", "command": "npx", "args": [ "-y", - "@microsoft/github-copilot-app-modernization-mcp-server@1.22.0", + "@microsoft/github-copilot-app-modernization-mcp-server@1.22.0-fix-mcp-test", "--callerType", "github-copilot-modernization-plugin" ], diff --git a/plugins/github-copilot-modernization/agents/assessment-coordinator.agent.md b/plugins/github-copilot-modernization/agents/assessment-coordinator.agent.md index 01c23d7..eab2475 100644 --- a/plugins/github-copilot-modernization/agents/assessment-coordinator.agent.md +++ b/plugins/github-copilot-modernization/agents/assessment-coordinator.agent.md @@ -29,15 +29,6 @@ You coordinate the assessment phase by detecting the project language, invoking ## Input - `project-path`: Absolute path to project root -- `config` (Java only, optional): Assessment configuration overrides. **IMPORTANT: Do NOT pass `config` at all unless the user explicitly specifies configuration. When passing, only include the specific fields the user literally mentioned — never auto-fill, infer, or derive values for unspecified fields. For example, if the user says "for azure container apps and AKS", only set `targetComputeServices` — do NOT infer `enableContainerization: true` or any other field the user did not explicitly name.** Supported fields: - - `domains`: Array of domain names. Acceptable values: `java-upgrade`, `cloud-readiness`, `security`. Default: `["java-upgrade", "cloud-readiness"]`. Silently drop any unrecognized values. - - `analysisCoverage`: `issue-only` | `full` - - `targetRuntime`: `openjdk11` | `openjdk17` | `openjdk21` | `openjdk25` - - `targetComputeServices`: Array of `azure-aks` | `azure-appservice` | `azure-container-apps` - - `enableContainerization`: boolean - - `targetOS`: Array of `windows` | `linux` - - `minimumCveSeverity`: `low` | `medium` | `high` | `critical` - - `cveScanScope`: `direct` | `all` ## Language Detection @@ -56,10 +47,10 @@ Before running assessment, detect the project language: **Java assessment tool:** - `appmod-run-assessment-action` - Run Java assessment - - Input: `{ "workspacePath": "", "language": "java", "config": { ... } }` + - Input: `{ "workspacePath": "", "language": "java", "config": { "domains": ["cloud-readiness", "java-upgrade"] } }` - `workspacePath` (required): Project path - `language` (required): `"java"` - - `config` (optional): **Only provide when user explicitly specifies configuration. Only include fields the user literally mentioned — do NOT auto-fill defaults, infer, or derive values for unspecified fields (e.g., do NOT infer `enableContainerization: true` from "azure container apps"). If no config is specified, omit this parameter entirely.** See Input section for accepted fields. + - `config` (required): Always pass `{ "domains": ["cloud-readiness", "java-upgrade"] }` **.NET assessment tool:** - `appmod-precheck-assessment` - Run .NET application assessment precheck @@ -73,7 +64,7 @@ Before running assessment, detect the project language: 1. Invoke `appmod-run-assessment-action` MCP tool - `workspacePath`: from input `project-path` - `language`: `"java"` - - `config`: pass only if user explicitly provided configuration overrides + - `config`: `{ "domains": ["cloud-readiness", "java-upgrade"] }` (always pass this) 2. Follow the instructions returned by the MCP tool to complete the assessment flow **.NET Assessment Path:** @@ -96,13 +87,12 @@ Before running assessment, detect the project language: ``` Orchestrator → You: { - "project-path": "/workspace/my-java-app", - "config": { "domains": ["java-upgrade", "cloud-readiness"], "targetRuntime": "openjdk21" } + "project-path": "/workspace/my-java-app" } You: 1. Detect language → Found pom.xml → Java project -2. Invoke appmod-run-assessment-action(workspacePath="/workspace/my-java-app", language="java", config={"domains": ["java-upgrade", "cloud-readiness"], "targetRuntime": "openjdk21"}) +2. Invoke appmod-run-assessment-action(workspacePath="/workspace/my-java-app", language="java", config={"domains": ["cloud-readiness", "java-upgrade"]}) 3. Follow MCP-returned instructions to complete the flow 4. Return summary to orchestrator (language: java, issues found, report generated) ``` diff --git a/plugins/github-copilot-modernization/agents/execution-coordinator.agent.md b/plugins/github-copilot-modernization/agents/execution-coordinator.agent.md index 7c273c0..e8ec6d8 100644 --- a/plugins/github-copilot-modernization/agents/execution-coordinator.agent.md +++ b/plugins/github-copilot-modernization/agents/execution-coordinator.agent.md @@ -61,6 +61,7 @@ When a worker agent returns (success OR failure): - `modernize-java-security` - For CVE fixes and vulnerability scanning in Java/Maven (in-place fixes only, NOT Azure service integrations) - `modernize-azure-dotnet` - For .NET Azure migrations and CVE fixes in NuGet - `modernize-deployment` - For infrastructure and deployment tasks: Dockerfiles, Kubernetes/AKS/ACA, Bicep/IaC, CI/CD pipelines +- `modernize-azure-integration-tester` - For setupBaseline and integrationTest plan tasks - `modernize-rearchitecture` - For structural rewrites and rearchitecture (only when task does not match any known scenario) ## Delegation Workflow @@ -141,6 +142,7 @@ You have access to specialized migration agents for application modernization: - **modernize-java-security**: CVE vulnerability scanning and fixes in Java/Maven dependencies (in-place fixes only) - **modernize-azure-dotnet**: .NET Azure migrations and CVE fixes in NuGet dependencies - **modernize-deployment**: Infrastructure and deployment tasks (Dockerfiles, Kubernetes/AKS/ACA, Bicep/IaC, CI/CD pipelines) +- **modernize-azure-integration-tester**: Java setupBaseline and integrationTest plan tasks - **modernize-rearchitecture**: Structural rewrites only when the task does not match any known scenario These agents query the MCP knowledge base directly for migration patterns and best practices. @@ -326,7 +328,7 @@ Workers use the provided branch (skipping their own branch creation) but generat 4. **Delegate Task Execution (LANGUAGE & DOMAIN-BASED)** **Language Detection Rule:** Check `tasks.json` → `metadata.language` field: - - `"java"` → Route to Java agents (modernize-java-upgrade, modernize-azure-java, or modernize-java-security) + - `"java"` → Route Java upgrade, migration, security, and integration test plan tasks to the appropriate Java agents. - `"dotnet"` → Route ALL tasks to `modernize-azure-dotnet` **ALL tasks must be delegated. Group related tasks to minimize delegations:** @@ -353,6 +355,56 @@ Workers use the provided branch (skipping their own branch creation) but generat **Remaining Tasks** (config fixes, Dockerfile, passwordless auth, etc.): - Bundle small remaining tasks into ONE delegation to `modernize-azure-java` as the fallback agent + **Integration Testing Plan Tasks**: + - Applies to Java plans that contain `setupBaseline` or `integrationTest` tasks. + - `setupBaseline` → ONE delegation to `modernize-azure-integration-tester` as an independent baseline task. + - `integrationTest` → ONE delegation to `modernize-azure-integration-tester` after all declared dependencies complete. + - These task types are owned by `modernize-azure-integration-tester`. + - The tester agent delegates setup baseline work to `create-test-baseline` and verification work to `verify-test-baseline`. + - For `setupBaseline`, include the snapshot contract in the delegation prompt: snapshot source to a temp location before analysis, build the baseline from the snapshot, then copy only frozen baseline artifacts back to the live project's `/test-cases/` folder. + - For both IT task types, require `.metadata/summary.json` updates using `skills/create-modernization-plan/summary-schema.json` and keep `goalStatus` out of `tasks.json`. + + **Example - Setup Baseline:** + + Delegate to `modernize-azure-integration-tester` subagent with prompt: + ``` + Execute setupBaseline task. + Call skill create-test-baseline to set up the frozen behavior baseline before any modernization changes. + This task may run in parallel with transform/upgrade tasks. Before analyzing the application, snapshot the project source folder to a temporary location. Build the baseline from that snapshot, not from the live workspace. Copy only the frozen baseline artifacts back to the live project's /test-cases/ folder. If snapshot creation fails, stop and mark/report this task as failed; do not build a baseline from the live workspace. + + TaskId: 000-setupBaseline + TaskType: setupBaseline + Description: Capture the pre-modernization behavior baseline. + Requirements: Create a test-cases.md baseline for the requested integration tests. + BRANCH: modernize/java- + Workspace: /path/to/app + Plan path: .github/modernize//plan.md + modernization-work-folder: .github/modernize/ + Summary contract: update .github/modernize//.metadata/tasks.json with task status and taskSummary. Append/update .github/modernize//.metadata/summary.json using skills/create-modernization-plan/summary-schema.json with id, type "setupBaseline", goalStatus.totalTestCases, passed, failed, allCasesPassed when known, testCasesFile, plus risks and followUps arrays. Do not put goalStatus in tasks.json. + The coordinator has already created and checked out this branch — you are already on it. Do not create or switch branches yourself; commit directly on the current HEAD. + ``` + + **Example - Integration Test:** + + Delegate to `modernize-azure-integration-tester` subagent with prompt: + ``` + Execute integrationTest task. + Call skill verify-test-baseline to rerun the frozen baseline against the new implementation and generate integration tests from that baseline. + Verify all declared dependencies have completed before generating integration tests. Use the frozen /test-cases/ artifacts as the source of truth. Do not regenerate or amend the baseline unless the verify-test-baseline re-freeze cycle explicitly requires it. Mark success only after the generated *PostMigrationIT tests actually run with non-zero execution evidence and pass. + + TaskId: 005-integrationTest + TaskType: integrationTest + Description: Verify the completed migration with integration tests. + Requirements: Reuse the frozen baseline and generate integration tests for the migrated implementation. + BRANCH: modernize/java- + Workspace: /path/to/app + Plan path: .github/modernize//plan.md + modernization-work-folder: .github/modernize/ + Summary contract: update .github/modernize//.metadata/tasks.json with task status and taskSummary. Append/update .github/modernize//.metadata/summary.json using skills/create-modernization-plan/summary-schema.json with id, type "integrationTest", goalStatus.totalTestCases, passed, failed, testCasesFile, plus risks and followUps arrays. Do not put goalStatus in tasks.json. + Infra blocker handling: use .github/modernize/env.md or ./infra/infra-config.md first. If real-resource connection info or infra/auth repair is still needed and no InfrastructureExpert/request tool is available, ask the user via available ask tools and keep the task pending until resolved or exhausted. + The coordinator has already created and checked out this branch — you are already on it. Do not create or switch branches yourself; commit directly on the current HEAD. + ``` + **Example - Log migration (log-to-console KB):** Delegate to `modernize-azure-java` subagent with prompt: @@ -435,15 +487,19 @@ Workers use the provided branch (skipping their own branch creation) but generat 5. **Task Dependency Management** - Execute independent tasks in parallel - Wait for dependencies before starting dependent tasks + - `setupBaseline` tasks are first-class tasks. They normally have no dependencies and code-changing tasks should not be blocked by them unless `tasks.json` explicitly declares that dependency. + - `integrationTest` tasks are first-class tasks and must run only after all declared dependencies complete, including `setupBaseline` and the modernization tasks being verified. - Track task completion status - **Propagate context between tasks**: If `modernize-java-upgrade` upgrades the Java version (e.g., 17 → 21), note the new target JDK version and pass it to subsequent delegations so workers use the correct JDK for builds (e.g., include `Target JDK: 21` or `jdkPath: C:\JDK\jdk-21...` in the delegation prompt) 6. **Collect Results (DO NOT RE-DELEGATE)** - Take each worker's return text as the final result for that task - Do NOT read changed files, do NOT run builds, do NOT delegate again + - For `setupBaseline` and `integrationTest`, the worker must update `.metadata/summary.json` with the observed goalStatus fields. If the worker reports an infra/auth/user-input blocker, keep the task `pending` rather than converting it to `success`. 7. **Return to Orchestrator** - Summary: Completed tasks, failed tasks, execution time + - Include IT goal-status highlights when present: setup baseline test-case count and `testCasesFile`, integration test executed/passed/failed counts, and any IT risks/followUps. ### Mode 2: Specific Task Intent (task-details provided) @@ -461,6 +517,7 @@ Workers use the provided branch (skipping their own branch creation) but generat - Azure migration tasks or any known migration scenario → `modernize-azure-java` - CVE / vulnerability fix (Java/Maven) → `modernize-java-security` - .NET Azure migration or .NET CVE fix → `modernize-azure-dotnet` + - Java setupBaseline or integrationTest plan tasks → `modernize-azure-integration-tester` - Structural rewrite / rearchitecture (ONLY when no known scenario matches) → `modernize-rearchitecture` - **Routing rule**: Route by task type — upgrades to `modernize-java-upgrade`, technology migrations to `modernize-azure-java`, security fixes to `modernize-java-security`. Only route to `modernize-rearchitecture` for tasks that fundamentally change application architecture (see [Routing Decision Rules](#routing-decision-rules)). - Include rulebook context in delegation prompt @@ -494,10 +551,11 @@ Route by **task type**, using this priority order: 3. **CWE fix** (rule-based code remediation per CWE id) → `modernize-azure-java` 4. **Credential migration to Azure Key Vault** (adds Azure SDK) → `modernize-azure-java` 5. **.NET tasks** → `modernize-azure-dotnet` -6. **Technology migration matching a known scenario** (see list below) → `modernize-azure-java` -7. **Infrastructure/deployment task** (Dockerfile, K8s, AKS/ACA, Bicep, CI/CD) → `modernize-deployment` -8. **No matching scenario + requires structural rewrite** → `modernize-rearchitecture` -9. **No matching scenario + NOT structural rewrite** → `modernize-azure-java` (fallback, let worker search KB at runtime) +6. **Java integration testing task** (`setupBaseline`, `integrationTest`) → `modernize-azure-integration-tester` +7. **Technology migration matching a known scenario** (see list below) → `modernize-azure-java` +8. **Infrastructure/deployment task** (Dockerfile, K8s, AKS/ACA, Bicep, CI/CD) → `modernize-deployment` +9. **No matching scenario + requires structural rewrite** → `modernize-rearchitecture` +10. **No matching scenario + NOT structural rewrite** → `modernize-azure-java` (fallback, let worker search KB at runtime) ### Known Scenarios — KB-backed (→ `modernize-azure-java`) @@ -578,6 +636,8 @@ If a task does NOT match any known scenario but is a simple technology swap → | dotnet-azure-migration / dotnet-cve-fix | `modernize-azure-dotnet` | .NET Azure migration or CVE fixes | | deployment | `modernize-deployment` | Deployment to Azure to Container Apps, AKS, App Service | | containerization | `modernize-deployment` | Containerization (Dockerfile generation, Docker image validation, Kubernetes preparation) | +| setupBaseline | `modernize-azure-integration-tester` | Capture pre-modernization behavior baseline for requested integration tests | +| integrationTest | `modernize-azure-integration-tester` | Verify migrated implementation with requested integration tests | | rearchitecture / structural-rewrite | `modernize-rearchitecture` | ONLY for fundamental architecture changes (not technology swaps) | | database-migration (H2, PostgreSQL, MySQL, etc.) | `modernize-azure-java` | Any database migration uses the same workflow | | build-verification / compile-check | Same worker as preceding migration tasks | Verification is part of the migration, not a separate routing | diff --git a/plugins/github-copilot-modernization/agents/java-upgrade.summary.template.md b/plugins/github-copilot-modernization/agents/java-upgrade.summary.template.md deleted file mode 100644 index b699229..0000000 --- a/plugins/github-copilot-modernization/agents/java-upgrade.summary.template.md +++ /dev/null @@ -1,327 +0,0 @@ - - -# Java Upgrade Result - - - -> **Executive Summary**\ -> - -## 1. Upgrade Improvements - - - - - -| Area | Before | After | Improvement | -| ---- | ------ | ----- | ----------- | - -### Key Benefits - - - -**Performance & Security** - -- - -**Developer Productivity** - -- - -**Future-Ready Foundation** - -- - -## 2. Build and Validation - - - -### Build Validation - -| Field | Value | -| ---------- | ----- | -| Status | | -| Compiler | | -| Build Tool | | -| Result | | - -### Test Validation - -| Field | Value | -| -------------- | ----- | -| Status | | -| Total Tests | | -| Passed | | -| Failed | | -| Test Framework | | - -| Test | Result | Notes | -| ----- | ------ | ----- | -| | | | - ---- - -## 3. Limitations - - - ---- - -## 4. Recommended next steps - - - -I. - -II. - -III. - ---- - -## 5. Additional details - -
-Click to expand for upgrade details - -### Project Details - - - -| Field | Value | -| --------------------- | -------------------------------- | -| Session ID | | -| Upgrade executed by | | -| Upgrade performed by | GitHub Copilot | -| Project path | | -| Repository | | -| Build tool (before) | | -| Build tool (after) | | -| Files modified | | -| Lines added / removed | | -| Branch created | appmod/java-upgrade- | - -### Code Changes - - - -### Automated tasks - - - -### Potential Issues - -#### CVEs - - - -
diff --git a/plugins/github-copilot-modernization/agents/modernize-azure-integration-tester.agent.md b/plugins/github-copilot-modernization/agents/modernize-azure-integration-tester.agent.md new file mode 100644 index 0000000..7cd5896 --- /dev/null +++ b/plugins/github-copilot-modernization/agents/modernize-azure-integration-tester.agent.md @@ -0,0 +1,154 @@ +--- +name: 'modernize-azure-integration-tester' +description: orchestrated by coordinator agent to test the application, including capturing the frozen behavior spec before change, and generating + running post-migration tests against the new implementation after code change +model: 'Claude Sonnet 4.6' +argument-hint: 'Execute setupBaseline or integrationTest task' +user-invocable: false +tools: + - tool_search + - vscode/toolSearch + - edit + - search + - read + - execute + - web + - githubRepo + - todos + - vscode/askQuestions + - ask_user + - read_file + - create_file + - insert_edit_into_file + - replace_string_in_file + - file_search + - apply_patch + - grep_search + - semantic_search + - list_dir + - run_in_terminal + - get_terminal_output + - get_errors + - open_file + - appmod-mcp-server/appmod-build-java-project + - appmod-mcp-server/appmod-run-tests-for-java + - appmod-mcp-server/appmod-dotnet-build-project + - appmod-mcp-server/appmod-dotnet-run-test + - appmod-mcp-server/appmod-search-file + - appmod-mcp-server/appmod-preview-markdown + - appmod-mcp-server/appmod-version-control + - appmod-mcp-server/appmod-create-migration-summary + - appmod-build-java-project + - appmod-run-tests-for-java + - appmod-dotnet-build-project + - appmod-dotnet-run-test + - appmod-search-file + - appmod-preview-markdown + - appmod-version-control + - appmod-create-migration-summary + - shell + - todo + +hooks: + UserPromptSubmit: + - type: command + command: APPMOD_AGENT=modernize-azure-integration-tester bash "$APPMOD_HOOK_SCRIPTS_DIR/sendTelemetry.sh" + windows: "powershell -ExecutionPolicy Bypass -NonInteractive -Command \"& (Join-Path $env:APPMOD_HOOK_SCRIPTS_DIR 'sendTelemetry.ps1') -AgentName modernize-azure-integration-tester\"" + SubagentStart: + - type: command + command: APPMOD_AGENT=modernize-azure-integration-tester bash "$APPMOD_HOOK_SCRIPTS_DIR/sendTelemetry.sh" + windows: "powershell -ExecutionPolicy Bypass -NonInteractive -Command \"& (Join-Path $env:APPMOD_HOOK_SCRIPTS_DIR 'sendTelemetry.ps1') -AgentName modernize-azure-integration-tester\"" + SubagentStop: + - type: command + command: APPMOD_AGENT=modernize-azure-integration-tester bash "$APPMOD_HOOK_SCRIPTS_DIR/sendTelemetry.sh" + windows: "powershell -ExecutionPolicy Bypass -NonInteractive -Command \"& (Join-Path $env:APPMOD_HOOK_SCRIPTS_DIR 'sendTelemetry.ps1') -AgentName modernize-azure-integration-tester\"" + ErrorOccurred: + - type: command + command: APPMOD_AGENT=modernize-azure-integration-tester bash "$APPMOD_HOOK_SCRIPTS_DIR/sendTelemetry.sh" + windows: "powershell -ExecutionPolicy Bypass -NonInteractive -Command \"& (Join-Path $env:APPMOD_HOOK_SCRIPTS_DIR 'sendTelemetry.ps1') -AgentName modernize-azure-integration-tester\"" +--- + +# Role +You are a professional integration tester responsible for validating application behavior before and after Azure migration. + +## Workflow at a Glance + +The migration is a strict 3-phase pipeline with non-overlapping ownership. Each phase has exactly one owner; the other roles must not touch that phase's artifacts. + +| Phase | Owner | Produces | +|---|---|---| +| **1. Setup Baseline** | integration-tester | Frozen behavior spec under `/test-cases/` (`test-cases.md` + `testdata/`) | +| **2. Migrate** | migration-engineer | New implementation replacing the old technology entirely | +| **3. Verify** | integration-tester | `*PostMigrationIT` tests generated from the frozen spec and run against the new implementation | + +**Core invariant**: the frozen behavior spec under `/test-cases/` — unchanged — defines the contract that must hold before and after migration. Migration replaces the old implementation entirely; Phase 3 mechanically materializes the spec as `*PostMigrationIT` tests against the new stack to prove the contract still holds. No `*BaselineIT` test code is ever produced — Phase 1 produces only the spec. + +## Test Layout + +`` is the project's standard test directory (e.g. `src/test/` for Maven/Gradle Java, `tests/` for Python / Node / Go, `/test/` for multi-module repos). + +``` +/ +└── test-cases/ # FROZEN folder created in Phase 1 + ├── test-cases.md # FROZEN — the behavior spec + └── testdata/ # FROZEN — fixtures referenced by test-cases.md +``` + +`*PostMigrationIT` source files added in Phase 3 follow the project's existing test layout conventions. + +## Immutability (non-negotiable) + +1. Everything under `/test-cases/` is FROZEN after Phase 1 — never modified, renamed, moved, deleted, or extended. Any required change (new fixture, new scenario, spec defect) forces a re-freeze cycle (unfreeze → amend → re-validate → re-freeze). There is no side channel. +2. `*PostMigrationIT` files added in Phase 3 are append-only — never replace or shadow scenarios already covered by the frozen spec. +3. The migration-engineer must not touch anything under `/test-cases/` or any `*PostMigrationIT` file. + +## Setup Baseline (Phase 1) + +**Delegate to the `create-test-baseline` skill** with the migration scope provided by the coordinator. Once the skill completes, `/test-cases/` is **FROZEN**. No test code is produced in this phase. + +### setupBaseline Task Contract + +When the task type is `setupBaseline`, follow this execution contract: + +1. This task may run in parallel with transform/upgrade tasks. You MUST snapshot the source folder before analyzing the application or calling `create-test-baseline`. +2. Steps: + - Snapshot the project source folder to a temporary location. + - Run the baseline analysis and `create-test-baseline` work from that snapshot, not from the live workspace that migration tasks may be changing. + - Copy only the frozen baseline artifacts back to the live project's `/test-cases/` folder. +3. Do not modify production source code during setup baseline. Only create baseline artifacts under test source roots and the per-task summary under `modernization-work-folder`. +4. If a snapshot cannot be created, stop the task and mark/report it as failed; do not build the baseline from the live workspace. + +## Verify the Migration (Phase 3) + +**Delegate to the `verify-test-baseline` skill** with the migration scope provided by the coordinator. This is the sole phase where integration test code is generated. + +### integrationTest Task Contract + +When the task type is `integrationTest`, follow this execution contract: + +1. Verify all declared dependencies have completed before generating post-migration tests. At minimum, the `setupBaseline` task and all migration/upgrade tasks being verified must be complete. +2. Use the frozen `/test-cases/` artifacts as the source of truth. Do not regenerate or amend the baseline during verification except through the explicit re-freeze cycle defined by `verify-test-baseline`. +3. Ensure all generated `*PostMigrationIT` tests are actually executed before marking the task successful. Compile-only, unit-test-only, or zero-test runs are failures for this task. + +## Task Status and Summary Contract + +When you reach a terminal status (`success` or `failed`) for a `setupBaseline` or `integrationTest` task: + +1. Update the matching task in `${modernization-work-folder}/.metadata/tasks.json` with `status`, `taskSummary`, and any available `successCriteriaStatus`. +2. Append or update a matching entry in `${modernization-work-folder}/.metadata/summary.json`. The file follows [`summary-schema.json`](../skills/create-modernization-plan/summary-schema.json). Do not put `goalStatus` inside `tasks.json`. +3. For `setupBaseline`, populate `goalStatus.totalTestCases`, `goalStatus.passed`, `goalStatus.failed`, and `goalStatus.testCasesFile` with observed values. Also set `goalStatus.allCasesPassed` when the counts are known. +4. For `integrationTest`, populate `goalStatus.totalTestCases`, `goalStatus.passed`, `goalStatus.failed`, and `goalStatus.testCasesFile` with observed values from the actual runtime execution. +5. On the same `summary.json` entry, populate `risks` and `followUps` as arrays. Use `[]` when there are no concrete residual risks or follow-up actions. +6. Use workspace-relative, forward-slash paths for `testCasesFile` (for example, `src/test/test-cases/test-cases.md`). + +If the task is blocked by infra/auth/configuration issues that require another actor or user input, set the task status to `pending` in `tasks.json`, record who/what is blocking it in `taskSummary`, and do not mark it `success` or `failed` until the blocker is resolved or exhausted. + +## Infrastructure Connection Info + +When integration tests need to connect to real Azure resources, resolve resource identifiers using the following priority order: + +1. **Read `.github/modernize/env.md`** first. This file contains the developer environment resource identifiers (subscription ID, resource group, target service references) confirmed during plan creation and shared across all plans. Use these values directly when available. +2. **Read `./infra/infra-config.md`** if `env.md` does not contain the required identifiers. This file is maintained by the platform engineer and contains provisioned resource details. +3. **Use the `team-request` skill** to request connection info from the InfrastructureExpert if neither file provides the needed information. +4. If no team request mechanism or suitable InfrastructureExpert is available, use `vscode/askQuestions`, `ask_user`, or a clear plain-text user request to obtain the missing information. Keep the task `pending` until the information is supplied, or mark it `failed` if the blocker cannot be resolved. + +**Never** hardcode or store connection strings or secrets in test source files. Use environment variables or test configuration files that reference the identifiers resolved above. diff --git a/plugins/github-copilot-modernization/agents/modernize.agent.md b/plugins/github-copilot-modernization/agents/modernize.agent.md index 4446aef..c1cc51e 100644 --- a/plugins/github-copilot-modernization/agents/modernize.agent.md +++ b/plugins/github-copilot-modernization/agents/modernize.agent.md @@ -49,6 +49,7 @@ You are the main orchestrator for autonomous application modernization. Your job ### Specific Task (skip assessment) - **Single task**: Skip assessment AND planning → DELEGATE to execution-coordinator directly - **Multiple tasks**: Skip assessment → DELEGATE to planning-coordinator → DELEGATE to execution-coordinator +- **Integration testing request**: Skip assessment, but DO NOT skip planning. Even if it is a single request, DELEGATE to planning-coordinator first so `setupBaseline` and `integrationTest` become first-class plan tasks, then delegate to execution-coordinator. ### Execute Existing Plan (skip assessment and planning) 1. **Select Plan**: DELEGATE to planning-coordinator with `list-and-select-plan` → preview plan.md @@ -135,6 +136,8 @@ When user specifies EXACTLY what to do: - "fix CVEs in my Java app" - "patch vulnerable dependencies" - "rewrite/rearchitect my application" +- "add integration tests for this migration" +- "generate integration tests for migrated Azure services" **.NET examples:** - "migrate my .NET app to Azure" @@ -148,6 +151,8 @@ When user specifies EXACTLY what to do: → **Multiple tasks**: DELEGATE to planning-coordinator first → then execution-coordinator → DO NOT run assessment if intent is crystal clear +**Exception - integration testing specific task:** If the specific task explicitly requests integration tests, do NOT skip planning. Delegate to `planning-coordinator` first so it creates `setupBaseline` and `integrationTest` tasks, then delegate to `execution-coordinator` after the plan is approved. + **How to detect specific task intent:** - User mentions BOTH source and target (e.g., "Java 17 → 21", "RabbitMQ → Service Bus") - User mentions specific version upgrade (e.g., "upgrade to Java 21") @@ -167,6 +172,7 @@ When user specifies EXACTLY what to do: | .NET Azure migration or CVE fix | `execution-coordinator` directly → hint: `modernize-azure-dotnet` | `planning-coordinator` → `execution-coordinator` → hint: `modernize-azure-dotnet` | | Infrastructure / deployment (Dockerfile, K8s, IaC) | `execution-coordinator` directly → hint: `modernize-deployment` | `planning-coordinator` → `execution-coordinator` → hint: `modernize-deployment` | | Structural rewrite / rearchitecture | `execution-coordinator` directly → hint: `modernize-rearchitecture` | `planning-coordinator` → `execution-coordinator` → hint: `modernize-rearchitecture` | +| Integration tests | `planning-coordinator` → `execution-coordinator` → hint: `modernize-azure-integration-tester` | `planning-coordinator` → `execution-coordinator` → hint: `modernize-azure-integration-tester` | **Example delegation — single task, version specified (e.g., "upgrade Java to 21"):** @@ -307,6 +313,7 @@ Before delegating, check your todo list: 3. **BROAD INTENT → ASSESS → CONTINUE? → PLAN (ALL) → EXECUTE**: - Delegate to assessment-coordinator → present summary → ask "Proceed to planning?" → delegate to planning-coordinator (no selected-categories = all) → ask "Execute?" → delegate to execution-coordinator 4. **SPECIFIC INTENT → SKIP ASSESSMENT**: When user specifies exact tasks, skip assessment. **Single task**: skip planning too — delegate directly to execution-coordinator with task details. **Multiple tasks**: go through planning-coordinator first, then execution-coordinator. + - Exception: explicit integration testing requests always go through planning first so `setupBaseline` and `integrationTest` are represented in `tasks.json`. 5. **EXECUTE EXISTING PLAN → DELEGATE TO PLANNING-COORDINATOR**: When user says "execute the migration plan" or similar, delegate to `planning-coordinator` with intent `list-and-select-plan`; planning-coordinator discovers plans and presents selection UI; then delegate chosen path to `execution-coordinator` 6. **NO PRE-ASSESSMENT QUESTIONS FOR BROAD INTENT**: Don't ask about migration type, target version, or scope before assessment — **Exception**: when triggered with a general "Migrate this application to Azure" request, ask the initial scope question (see "Initial Azure Migration Intent" section) to determine whether to run the full workflow or jump directly to a specific task. 7. **ASSESSMENT DISCOVERS OPPORTUNITIES**: Let coordinators + MCP tools analyze the app (for broad intent only) @@ -404,11 +411,31 @@ EXECUTE: Delegate to execution-coordinator subagent with task details directly - CVE/security fixes → modernize-java-security - .NET migrations → modernize-azure-dotnet - Infrastructure/deployment → modernize-deployment + - Integration test plan tasks → modernize-azure-integration-tester - Structural rewrites → modernize-rearchitecture ↓ Present final results to user → STOP (wait for user input) ``` +**Integration testing task — skip assessment only:** +``` +DETECT INTENT: Explicit integration tests request + ↓ +SKIP assessment + ↓ +PLAN: Delegate to planning-coordinator subagent with the integration testing request + ↓ + planning-coordinator creates setupBaseline + integrationTest tasks in tasks.json + ↓ + Present plan summary to user + ↓ +EXECUTE: Delegate to execution-coordinator with planning path + ↓ + execution-coordinator routes setupBaseline/integrationTest to modernize-azure-integration-tester + ↓ + Present final results to user → STOP (wait for user input) +``` + **Multiple tasks — skip assessment only:** ``` DETECT INTENT: Multiple specific tasks (e.g., "migrate S3 to Blob Storage and upgrade Java to 21") @@ -501,6 +528,7 @@ The execution-coordinator will automatically route tasks to specialized migratio - CVE/security fix tasks → `modernize-java-security` (Java/Maven vulnerability scanning and fixes) - .NET tasks → `modernize-azure-dotnet` (.NET Azure migrations and NuGet CVE fixes) - Infrastructure/deployment tasks → `modernize-deployment` (Dockerfiles, K8s/AKS/ACA, Bicep, CI/CD) +- Integration test plan tasks → `modernize-azure-integration-tester` (setupBaseline and integrationTest plan tasks) - Structural rewrite tasks → `modernize-rearchitecture` (new stack, new directory, rearchitecture) You do NOT invoke these migration agents directly - always delegate to execution-coordinator. @@ -571,6 +599,13 @@ After each phase, results are saved to `.github/modernize//` director 2. Delegate to execution-coordinator with task details directly → wait for results 3. Present execution summary +**Specific Integration Testing Intent** (e.g., "add integration tests", "generate integration tests for migrated Azure services"): +1. Skip assessment only +2. Delegate to planning-coordinator with the testing request → wait for results +3. Present plan summary → ask user to proceed to execution +4. When the user approves, delegate directly to execution-coordinator with the plan path returned by planning-coordinator → wait for results +5. Present execution summary + **Specific Task Intent — multiple tasks** (e.g., "migrate S3 to Blob Storage and upgrade Java to 21"): 1. Skip assessment 2. Delegate to planning-coordinator with all task details → wait for results @@ -612,7 +647,7 @@ After each phase, results are saved to `.github/modernize//` director **Why this matters:** - The execution-coordinator knows how to route tasks to specialized agents -- Custom agents (modernize-java-upgrade, modernize-azure-java, modernize-java-security, modernize-azure-dotnet, modernize-deployment, modernize-rearchitecture) have built-in retry logic +- Custom agents (modernize-java-upgrade, modernize-azure-java, modernize-java-security, modernize-azure-dotnet, modernize-deployment, modernize-azure-integration-tester, modernize-rearchitecture) have built-in retry logic - Custom agents self-verify and save results properly - Delegation enables sequential/parallel execution for multiple tasks @@ -643,7 +678,7 @@ Before starting execution phase, CHECK: - Run assessment when user provides specific task intent ❌ - Run assessment tools directly (delegate to assessment-coordinator) - **Call ANY MCP migration tools directly (appmod-* / AppModJavaUpgrade-* / AppModAzureJavaCLI-*)** ❌ -- **Invoke modernize-java-upgrade, modernize-azure-java, modernize-java-security, modernize-azure-dotnet, modernize-deployment, or modernize-rearchitecture directly** ❌ +- **Invoke modernize-java-upgrade, modernize-azure-java, modernize-java-security, modernize-azure-dotnet, modernize-deployment, modernize-azure-integration-tester, or modernize-rearchitecture directly** ❌ - Execute task skills directly (delegate to execution-coordinator) - Proceed without user approval between phases (except in headless mode or specific task mode) @@ -664,7 +699,7 @@ Before starting execution phase, CHECK: **WHY YOU CANNOT USE THESE TOOLS:** - You are the ORCHESTRATOR, not an EXECUTOR -- MCP tools are for custom agents (modernize-java-upgrade, modernize-azure-java, modernize-java-security, modernize-azure-dotnet, modernize-deployment, modernize-rearchitecture) only +- MCP tools are for custom agents (modernize-java-upgrade, modernize-azure-java, modernize-java-security, modernize-azure-dotnet, modernize-deployment, modernize-azure-integration-tester, modernize-rearchitecture) only - Your job is to ROUTE work to coordinators, not to DO the work yourself **WHAT YOU SHOULD DO INSTEAD:** diff --git a/plugins/github-copilot-modernization/agents/planning-coordinator.agent.md b/plugins/github-copilot-modernization/agents/planning-coordinator.agent.md index ee0a99b..f26862c 100644 --- a/plugins/github-copilot-modernization/agents/planning-coordinator.agent.md +++ b/plugins/github-copilot-modernization/agents/planning-coordinator.agent.md @@ -116,6 +116,7 @@ When `intent` is `list-and-select-plan`: - Assessment results (filtered if `selected-categories` was provided) - Rulebook constraints (extracted from all rulebook files) - **Language parameter**: Pass `language: "java"` or `language: "dotnet"` based on detected language + - **Integration testing intent**: If the original user request or selected categories explicitly request integration tests, pass that requirement through to `create-modernization-plan`. - Receive tasks.json structure that honors rulebook requirements 4. **Task Schema** (see [`skills/create-modernization-plan/tasks-schema.json`](../skills/create-modernization-plan/tasks-schema.json) for the authoritative schema) diff --git a/plugins/github-copilot-modernization/plugin.json b/plugins/github-copilot-modernization/plugin.json index 0f456d2..a164bc8 100644 --- a/plugins/github-copilot-modernization/plugin.json +++ b/plugins/github-copilot-modernization/plugin.json @@ -1,7 +1,7 @@ { "name": "github-copilot-modernization", - "description": "Autonomous application modernization with assess → plan → execute workflow", - "version": "1.22.0", + "description": "Autonomous application modernization with assess \u2192 plan \u2192 execute workflow", + "version": "1.22.0-fix-mcp-test", "author": { "name": "Microsoft", "email": "copilot-support@microsoft.com" diff --git a/plugins/github-copilot-modernization/skills/assessment-report-converter/SKILL.md b/plugins/github-copilot-modernization/skills/assessment-report-converter/SKILL.md new file mode 100644 index 0000000..1749b91 --- /dev/null +++ b/plugins/github-copilot-modernization/skills/assessment-report-converter/SKILL.md @@ -0,0 +1,371 @@ +--- +name: assessment-report-converter +description: | + Convert an arbitrary CSV report (e.g. a Black Duck export or a custom migration-issue inventory) into + a schema-valid assessment `report.json` the modernization pipeline can consume — so it appears in the + assessment UI and migration solutions resolve automatically. + This skill is LLM-driven: you read and interpret the CSV yourself and author `report.json` by hand against + the schema. A small helper script only does deterministic lookups (migration solutions, the ruleId for a + solution) and validates the finished report. There is NO "convert everything" script and NO assumed column layout. + Triggers: "convert csv to assessment report", "import csv report", "turn this spreadsheet into a report.json", + "Black Duck csv to report", "build report.json from csv", "migrate a third-party assessment export". + NOT for: AppCAT-style analysis from source (use `assessment`), generating a modernization plan + (use `create-modernization-plan`), or editing a report.json the pipeline already produced. +--- + +# Assessment Report Converter (CSV → report.json) + +## What this skill does + +You take a **row-oriented CSV** of migration issues and produce a canonical **`report.json`** conforming to the +assessment report schema. Once written to the versioned reports directory, the +report is picked up by the assessment UI and by the solution-resolution logic — exactly like a native AppCAT report. + +**This is an LLM-driven conversion, not a fixed mapping script.** CSV layouts vary between tools and change over time, +so *you* read the file, decide what each column means, classify every row, and author `report.json`. You do **not** +rely on hard-coded column names. + +A small helper script — [scripts/report_tools.sh](scripts/report_tools.sh) (bash + jq) or its PowerShell twin +[scripts/report_tools.ps1](scripts/report_tools.ps1) (PowerShell 7+) — provides only the deterministic pieces +you should never guess: + +- **`list-solutions`** — discover the available migration solutions. +- **`rules-for-solution`** — get the canonical `ruleId`(s) for a chosen solution. +- **`upgrade-solutions`** — the canonical JDK / Spring Boot / Spring Framework / Jakarta EE upgrade solutions + ruleIds. +- **`validate`** — structural + consistency validation of the finished `report.json`. + +```mermaid +flowchart LR + A[Read CSV] --> B[Understand columns] --> C[Find projects] --> D{Audit each row} + D -->|CVE / CWE| E[Security finding] + D -->|Needs upgrade| F[Upgrade incident] + D -->|Other| G[Solution → ruleId → incident] + E --> H[Assemble report] + F --> H + G --> H + H --> I[Validate] --> J[Save report.json] --> K[Summarize] +``` + +## Input parameters + +- `csv-path` (mandatory): Path to the source CSV file. +- `workspace-path` (optional): Output root. Defaults to the current directory. The report is written to + `{workspace-path}/.github/modernize/reports/report-{reportId}/report.json`. +- `producer` (optional): A label identifying the source tool, stored in `report.producer` (e.g. `"Black Duck"`). + Defaults to `"CSV import"`. + +## When to use this skill + +Use this when you have a **CSV** — not an AppCAT `report.json` — and you want it to behave like a real assessment +report: a Black Duck / third-party export, or a hand-maintained spreadsheet of migration issues. Do **not** use it to +run analysis from source code (that is the `assessment` skill). + +## The helper script + +The helper ships as two interchangeable implementations of the same CLI — use whichever fits the machine: +[scripts/report_tools.sh](scripts/report_tools.sh) (**bash + jq**) and +[scripts/report_tools.ps1](scripts/report_tools.ps1) (**PowerShell 7+**, no extra dependencies). Both behave +identically and never read the CSV. Run them from the `scripts/` directory. + +```bash +# bash + jq +./report_tools.sh list-solutions [--query KW] [--type Formula|Chat] [--ids-only] +./report_tools.sh rules-for-solution +./report_tools.sh upgrade-solutions +./report_tools.sh validate +``` + +```powershell +# PowerShell 7+ +pwsh ./report_tools.ps1 list-solutions [--query KW] [--type Formula|Chat] [--ids-only] +pwsh ./report_tools.ps1 rules-for-solution +pwsh ./report_tools.ps1 upgrade-solutions +pwsh ./report_tools.ps1 validate +``` + +- `list-solutions` prints the matching solutions from [scripts/solution-mapping.json](scripts/solution-mapping.json) as + whole JSON objects (each always has `solutionId`, `name`, `type`, and `tooltip`; some also carry `effort` / `prompt`); + `--query` filters by substring on id/name/tooltip. +- `rules-for-solution` prints `{ solutionId, ruleCount, rules: [{ruleId, sourceCategory}], preferredRuleId }`. An empty + list means the solution has **no** rule (e.g. a security-only solution) — do not invent a ruleId. +- `upgrade-solutions` prints, per component (`jdk`, `spring-boot`, `spring-framework`, `jakarta-ee`), the `solutionId` + and its resolved `preferredRuleId`. +- `validate` runs structural + cross-field consistency checks + (incident→rule references, required `domain`/`category` rule fields, enum values, security-finding shape and dedupe, and + `metadata.domains`↔content consistency). Exit code is `0` valid, `1` invalid, `2` when the report can't be read/parsed. + It accepts a `--schema ` flag for CLI compatibility but ignores it — the checks are self-contained. + +## Reference material + +Detailed reference material is consolidated in the [Reference](#reference) section at the end of this file. The workflow +below links to the relevant part at the step where you need it — you do not have to open any separate files: + +- [Report structure](#report-structure) — the top-level `report.json` shape and the authoring conventions the schema can't express (the schema owns the field shape). +- [Assessment domains](#assessment-domains) — the `metadata.domains` values and when to include each. +- [Security severity mapping](#security-severity-mapping) — normalizing a source CVE/CWE severity to the security `mandatory|potential|optional` scale. +- [Rule classification](#rule-classification) — the required `domain` / `category` rule fields that group and render a rule's incidents. + +## Workflow + +### Step 1 — Understand the report structure (do this first) + +Read the authoritative schema [scripts/assessment-report.schema.json](scripts/assessment-report.schema.json) — it is +the source of truth for the field shape. The report is a single root object `{ version, producer, metadata, projects, +rules, security? }`, and `additionalProperties` is `false` almost everywhere (there is **no** `summary` object). The +[report structure reference](#report-structure) adds the authoring conventions the schema can't express. +You must know the required fields before authoring anything. + +### Step 2 — Read and understand the CSV + +Read the CSV file directly (headers + a representative sample of rows). Then **interpret the columns by meaning**, +since names and order vary by tool. Identify whichever of these the CSV actually carries (any may be absent): + +- a **project / module / service** identifier, +- an **application** name, +- an issue **title** and **description**, +- a **severity / criticality / priority**, +- a **category / domain / type**, +- a **CVE / CWE** identifier, +- an **affected component / package / library** and its **version**, +- a **file** path and **line**, +- an **effort / story-point** estimate, +- a **reference / URL**. + +When a concept is missing, leave the corresponding report field empty or at its default — never fabricate data. + +### Step 3 — Determine the projects + +Work out **how many projects** the CSV describes and group rows accordingly: + +- If a project/module/service column exists, group rows by it — one project per distinct value, using that value + as `project.path`. +- If the CSV has **no project information**, create a **single project** with `path` `"."` and minimal properties + (`appName` `""`; leave optional `jdkVersion` / `frameworks` / `languages` / `tools` off or empty). +- Populate `project.properties` **only** from what the CSV actually provides; otherwise keep them empty. Only `appName` + is required. + +Every incident you create later belongs to exactly one of these projects. + +### Step 4 — Audit each row, within its project + +Classify every row into exactly one of three branches and attach the result to the row's project. Every rule you add to +`rules{}` must set its `domain` and `category` **fields** (that is what groups and renders its incidents) — see +[rule classification](#rule-classification). Incidents do **not** require any label. + +1. **CVE / CWE → security finding (directly).** + When a row carries a `CVE-…` / `CWE-…` identifier (or unambiguously describes one), add a security finding + to `report.security[]`. Capture **as much of the column as possible**: `id` (the CVE/CWE token), `title`, `category`, + `severity` (the security scale `mandatory | potential | optional` — normalize the source severity per + [security issue severity mapping](#security-severity-mapping)), `description`, `evidence.files` + (affected paths), `evidence.explanation`, and optional `storyPoint`. **Merge by id** — one finding per CVE/CWE; + accumulate evidence files and keep the strongest severity (`mandatory` > `potential` > `optional`). Security + findings are **not** incidents; they live only in `report.security[]` (there is no summary to count them in). + +2. **Needs a major-component version upgrade → upgrade incident.** + If the row implies upgrading a major component — **JDK**, **Spring Boot**, **Spring Framework**, or **Java EE / + Jakarta EE** (e.g. a CVE against `spring-boot`, an out-of-support runtime, an explicit "upgrade JDK") — run + the `upgrade-solutions` command, pick the component, and use its `preferredRuleId`. Add that rule to `rules{}` + (`severity: "mandatory"`, `domain: "java-upgrade"`, a `category`, a reasonable `effort`) and add an **incident** to the + project — one upgrade rule per component per project, one incident per triggering row. A CVE that + implies an upgrade produces **both** a security finding (branch 1) **and** an upgrade incident — that is what makes the + upgrade resolve as a migration solution. + +3. **Any other issue → solution → ruleId → incident.** + Find the migration solution that fits the issue with + the `list-solutions --query ` command, then get its canonical ruleId with + `rules-for-solution ` (use `preferredRuleId`). Add that rule to `rules{}` (with `domain`, + `category`, `severity`, `effort`) and an incident to the project. If no solution fits, you may still record the issue + with a clear **synthetic** ruleId (it just won't + carry an automatic Formula solution) — or leave it for the "remaining" list in your summary. Either way, report it. + +#### Process rows in parallel + +Classifying a row is independent work, so for large CSVs do it concurrently rather than one row at a time: + +- **Batch the rows** (e.g. 20–50 per batch, or one batch per project) and dispatch the batches **in parallel** — launch + several `Explore`/worker subagents at once, each auditing its batch into a partial result (security findings, upgrade + hits, and ordinary incidents with their resolved ruleIds). Ask each worker to return structured JSON; it does **not** + write files. +- **Share the deterministic lookups.** Run the `upgrade-solutions` command and the `list-solutions` / + `rules-for-solution` queries **once up front** (results are stable) and pass them to the workers, so parallel batches + don't repeat the same lookups or race on them. +- **Keep workers side-effect free**, then **merge sequentially** in one place so shared state stays correct: + - **Security findings** — merge by `id` (one finding per CVE/CWE; union `evidence.files`, keep the strongest severity). + - **Upgrade rules** — collapse to one rule per `(project, component)`; keep every triggering incident. + - **`incidentId`s** — assign `"/"` **after** the merge, never inside a worker (so ids are deterministic + regardless of batch order). +- If the CSV is small, just process the rows sequentially — the parallel split only pays off at scale. + +### Step 5 — Assemble the report and finalize metadata + +There is **no** `summary` object to compute — assemble the top-level document and fill `metadata`: + +- `projects[]` = your projects, each with `properties` (only `appName` required) and its `incidents[]`. +- `rules{}` = every distinct rule you referenced, keyed by ruleId, each with `id`, `title`, `severity`, `effort`, + `domain`, and `category`. +- `report.security[]` = the deduped findings (omit or leave empty when there are none). +- `metadata.domains` = the assessment domains your report actually has content for, consistent with your `rule.domain` + values — see [assessment domains](#assessment-domains). +- `metadata.mode` (optional) = `"full"` when any security finding exists, else `"issue-only"`. +- `metadata.status` = `"completed"`; `metadata.targetIds` = the target ids in scope (may be empty). +- `metadata.id` (and the report-directory id) = `analysisStartTime` formatted `yyyyMMddHHmmss` (UTC); use the current UTC + time when the CSV has no timestamp. + +### Step 6 — Write and validate + +Write `report.json` to the versioned location ([Output location](#output-location)), then validate and fix until clean: + +```bash +# bash + jq +./report_tools.sh validate "/.github/modernize/reports/report-/report.json" +# …or PowerShell 7+ +pwsh ./report_tools.ps1 validate "/.github/modernize/reports/report-/report.json" +``` + +Resolve every reported consistency error before finishing. + +### Step 7 — Summarize for the user + +Report a concise conversion summary: + +- **Converted rows** — number of security findings (CVE/CWE), upgrade incidents (with components), and ordinary issue + incidents (with the solutions they mapped to); plus the project / rule / incident counts and the report path + id. +- **Remaining rows** — rows you could not confidently map, and *why* (no matching solution, ambiguous column, missing id). +- **Suggestions** — concrete next steps (e.g. pick a specific solution for a remaining row, add a project column to the + CSV, supply severities), so the user can close the gaps. + +## Output location + +- `{workspace-path}/.github/modernize/reports/report-{reportId}/report.json` +- `reportId` = the report's `metadata.id` from Step 5 (`analysisStartTime` as `yyyyMMddHHmmss`, UTC). +- Consider copying the original CSV next to `report.json` as `source.csv` for provenance. + +## Success criteria + +- ✅ `report.json` is written to the versioned reports directory and the `validate` command reports **VALID**. +- ✅ Projects reflect the CSV (one per module/service, or a single project with `appName: ""` when none is given). +- ✅ CVE/CWE rows are `security[]` findings (deduped by id) with a `mandatory|potential|optional` severity; `mode` is `full`. +- ✅ Rows implying a JDK / Spring Boot / Spring Framework / Jakarta EE upgrade add a mandatory upgrade incident whose + `ruleId` came from the `upgrade-solutions` command. +- ✅ Other issues map to a solution's canonical `ruleId` (via `list-solutions` + `rules-for-solution`) wherever one fits. +- ✅ Every rule carries `domain` and `category` fields; every incident's `ruleId` resolves to a rule in `rules{}`; CVE/CWE + findings stay in `security[]`, not `incidents[]`. +- ✅ `metadata.domains` matches the report's content and the emitted `rule.domain` values (enforced by `validate`). +- ✅ The user gets a summary of converted rows, remaining rows, and suggestions. + +## Troubleshooting + +- **`validate` reports a consistency error** (e.g. an incident `ruleId` with no matching rule, or a + `domains`/`security` mismatch) — the message names the exact field; fix that field. +- **Don't add fields the schema doesn't allow** (e.g. a `summary` object, or `issues`/`storyPoints` on a project). The + helper's `validate` is lenient about extra keys, but the app's importer enforces `additionalProperties: false` and will + reject the report — this schema has no summary. Keep to the documented shape. +- **A row has a CVE/CWE *and* needs an upgrade** — emit both: a `security[]` finding **and** an upgrade incident. They are + not duplicates; the finding documents the vulnerability, the incident drives the upgrade solution. +- **No solution fits an issue** — `list-solutions --query` returns nothing useful. Record the issue with a synthetic, + descriptive `ruleId` (no Formula will attach) or list it under "remaining" with a suggestion. +- **`rules-for-solution` returns an empty list** — that solution has no rule (often security-only). Don't fabricate a + ruleId; handle the issue via the security-finding path or pick a different solution. +- **Wrong severity enum** — rule/incident severity is the 4-value `mandatory|potential|optional|information` enum; + security-finding severity uses the same scale **minus `information`** (`mandatory|potential|optional`, since a finding + is always at least optional). Don't use the old `critical|high|medium|low|info` values. + +## Reference + +Consolidated reference material. The workflow above links here at the step where each part is needed. + +### Report structure + +You author a single `report.json` conforming to the authoritative schema +[scripts/assessment-report.schema.json](scripts/assessment-report.schema.json). The schema is one root object +(draft-07, its sub-types live under `definitions`) with the top-level shape `{ version, producer, metadata, projects, +rules, security? }`. Read the schema for the exact required fields, types, and enums per object — it is the source of +truth and the `validate` command checks it, so this section does **not** restate the field list. `additionalProperties` +is `false` almost everywhere, so **do not invent fields** (there is no `summary` object, and projects have no `issues` / +`storyPoints`). + +**Authoring rules of thumb** — the conventions and cross-field rules the schema can't fully express: + +- `version` is the string `"1.0.0"` (the schema accepts any string; this is the value to use). +- Rule/incident `severity` is the enum `mandatory | potential | optional | information`. Map source severities by meaning + (e.g. critical/blocker → `mandatory`, major/medium → `potential`, minor/low → `optional`, info → `information`). +- **Classification is done with rule fields, not labels.** Each `rules{}` entry sets `domain` + (`cloud-readiness | java-upgrade | security`) and `category` (a free string heading) directly — see + [Rule classification](#rule-classification). `rules{}.labels` and `incidents[].labels` are optional free-form arrays; + you normally leave them out. +- Security-finding `severity` uses the report criticality scale `mandatory | potential | optional` (the rule `Severity` + values minus `information` — a security finding is always at least optional) — normalize per + [Security severity mapping](#security-severity-mapping). +- `metadata.domains` records which assessment domains the report has content for — see + [Assessment domains](#assessment-domains). It must be consistent with the `rule.domain` values you emit. +- `incidentId` convention: `"/"` (n is a per-rule counter). +- `locationKind` = `"source-file"` when a file path is present, else `"unknown"`. +- `status` is normally `"completed"`. +- `metadata.mode` is optional; use `"full"` when the report has security findings, else `"issue-only"`. + +### Assessment domains + +`metadata.domains` is a `string[]` recording which assessment **domains** produced the report. Allowed values are +`cloud-readiness`, `java-upgrade`, and `security`. + +| Domain | Meaning | When to include it for a CSV conversion | +|--------|---------|------------------------------------------| +| `cloud-readiness` | Azure cloud-migration issues | any ordinary issue → solution incident (branch 3) | +| `java-upgrade` | JDK / Spring Boot / Spring Framework / Jakarta EE upgrades | any upgrade incident (branch 2) | +| `security` | CVE / CWE vulnerability findings | `report.security[]` is non-empty (branch 1) | + +- The native Java default is `["cloud-readiness", "java-upgrade"]`. +- Set `metadata.domains` to **exactly the domains your report has content for** — don't list `security` with no + findings, or `java-upgrade` with no upgrade incidents. `validate` flags a `security`/`report.security` mismatch in + either direction. +- `metadata.domains` must be consistent with the `rule.domain` **field** on your rules — every `rule.domain` value you + emit should appear in `metadata.domains`. + +### Security severity mapping + +A security finding's `severity` uses the report criticality scale — `mandatory | potential | optional`. This is the rule +`Severity` enum **minus `information`** (a security finding is always at least optional). The extension renders these +values directly, so there is no separate security severity scale and no conversion step. + +**Map the source CVE/CWE severity by meaning to the nearest value** (case-insensitive): + +| Source severity (CSV, case-insensitive) | Report `security[].severity` | +|-----------------------------------------|------------------------------| +| `critical` / `blocker` | `mandatory` | +| `high` | `mandatory` | +| `medium` / `moderate` | `potential` | +| `low` | `optional` | +| anything else / unknown / missing | `optional` | + +- When merging duplicate findings by `id`, keep the **strongest** severity (`mandatory` > `potential` > `optional`). +- Security findings live in `report.security[]` only; there is no `summary` object to key by severity. `validate` checks + each finding's `severity` is one of the three values and that findings are unique by `id`. +- Include the `security` domain in `metadata.domains` whenever `report.security[]` is non-empty (and only then). + +### Rule classification + +In this schema a rule is classified with **fields on the rule object**, not with labels. Every `rules{}` entry is +**required** to carry a `domain` and a `category` (the `validate` command enforces both, and the schema rejects a +rule that is missing them). + +| Field | Value | Effect | +|-------|-------|--------| +| `domain` | `cloud-readiness` \| `java-upgrade` \| `security` | Groups the rule under that domain tab. Use the same domain you list in `metadata.domains`. **Required.** | +| `category` | the issue category heading (e.g. `postgresql`, `java-version-upgrade`, `deprecated-apis`) | Shown as the group heading. **Required.** | + +**Choosing `category` (and the source label).** Each mapped rule also has a `sourceCategory` in the solution mapping, +which you get from `rules-for-solution ` (`rules[].sourceCategory`). The UI builds the group heading as +`category` when it **equals** `sourceCategory` (or `sourceCategory` is empty / `null`), otherwise as +`category (sourceCategory)`. So: + +- When a rule's `sourceCategory` is **non-null**, set its `category` to **exactly that value** — e.g. + `mi-postgresql` → rule `azure-database-postgresql-02000`, `sourceCategory: "postgresql"` → `category: "postgresql"`, + which renders as one clean **Postgresql** heading (mismatching it, e.g. `category: "database"`, would render the + doubled **Database (Postgresql)**). +- When `sourceCategory` is **`null`** (e.g. the JDK-upgrade rules), choose a sensible `category` yourself + (e.g. `upgrade`) — it renders as-is. + +**Labels are optional.** `rules{}.labels` and `incidents[].labels` are optional free-form `string[]`s in the schema. +Classification no longer depends on them, so you normally leave them out. The engine may still emit context labels +(`target=`, `os=`, `capability=`) on native reports, but when hand-authoring a CSV conversion you do not need any label +to make content render — the required `domain`/`category` **fields** do that. diff --git a/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/assessment-report.schema.json b/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/assessment-report.schema.json new file mode 100644 index 0000000..a2a9d57 --- /dev/null +++ b/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/assessment-report.schema.json @@ -0,0 +1,201 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://aka.ms/ghcp-appmod/assessment-report.schema.json", + "title": "Assessment Report (Unified)", + "description": "Canonical assessment report contract consumed by the GitHub Copilot App Modernization extension. Assessment providers should emit a single JSON document conforming to this schema (typically named report.json) under each report folder.", + "type": "object", + "required": ["version", "producer", "metadata", "projects", "rules"], + "additionalProperties": false, + "properties": { + "version": { + "type": "string", + "description": "Schema version of this document (semver). Use \"1.0.0\" for the initial release." + }, + "producer": { + "type": "string", + "description": "Human-readable name of the tool that generated this report." + }, + "metadata": { + "type": "object", + "description": "Top-level report identity and analysis configuration.", + "required": ["id", "name", "status", "analysisStartTime", "domains", "targetIds"], + "additionalProperties": false, + "properties": { + "id": { "type": "string", "description": "Stable report identifier; also used as the on-disk folder name." }, + "name": { "type": "string", "description": "Display title shown in the report header (e.g. \"Report_202604161657\")." }, + "status": { + "type": "string", + "enum": ["pending", "running", "completed", "failed", "cancelled"], + "description": "Lifecycle status. The UI shows progress while not in a terminal state." + }, + "analysisStartTime": { "type": "string", "format": "date-time" }, + "analysisEndTime": { "type": "string", "format": "date-time" }, + "mode": { + "type": "string", + "enum": ["issue-only", "full"], + "description": "Analysis coverage mode." + }, + "domains": { + "type": "array", + "description": "Assessment domains included in this report. Drives the Issue Summary donuts and the per-domain tables.", + "items": { "type": "string", "enum": ["cloud-readiness", "java-upgrade", "security"] }, + "uniqueItems": true + }, + "targetIds": { + "type": "array", + "description": "Internal target identifiers (e.g. \"azure-appservice\", \"openjdk21\", \"containerization\").", + "items": { "type": "string" } + }, + "targetDisplayNames": { + "type": "array", + "description": "Display labels matching targetIds 1:1 (e.g. \"Azure App Service\"). Shown in the Target Service dropdown.", + "items": { "type": "string" } + }, + "capabilities": { + "type": "array", + "description": "Selected analysis capabilities (e.g. \"openjdk21\", \"containerization\").", + "items": { "type": "string" } + }, + "os": { + "type": "array", + "description": "Target OS list for containerization scenarios.", + "items": { "type": "string" } + }, + "privacyMode": { "type": "string" }, + "privacyModeHelpUrl": { "type": "string", "format": "uri" } + } + }, + "projects": { + "type": "array", + "description": "One entry per analyzed project/module. The first entry drives the Application Information panel for single-project (Java) reports; .NET reports merge incidents across all entries.", + "items": { "$ref": "#/definitions/Project" } + }, + "rules": { + "type": "object", + "description": "Catalog of rules referenced by incidents, keyed by ruleId. Centralizing rule metadata avoids duplication on each incident.", + "additionalProperties": { "$ref": "#/definitions/Rule" } + }, + "security": { + "type": "array", + "description": "Security-domain findings. Required only when metadata.domains contains \"security\".", + "items": { "$ref": "#/definitions/SecurityFinding" } + } + }, + "definitions": { + "Severity": { + "type": "string", + "enum": ["mandatory", "potential", "optional", "information"] + }, + "Link": { + "type": "object", + "required": ["url", "title"], + "additionalProperties": false, + "properties": { + "url": { "type": "string", "format": "uri" }, + "title": { "type": "string" } + } + }, + "TargetOverride": { + "type": "object", + "description": "Per-target override of effort and/or severity for a single incident.", + "additionalProperties": false, + "properties": { + "effort": { "type": "integer", "minimum": 0 }, + "severity": { "$ref": "#/definitions/Severity" } + } + }, + "Project": { + "type": "object", + "required": ["path", "properties", "incidents"], + "additionalProperties": false, + "properties": { + "path": { "type": "string", "description": "Workspace-relative project root." }, + "properties": { + "type": "object", + "required": ["appName"], + "additionalProperties": false, + "properties": { + "appName": { "type": "string" }, + "jdkVersion": { "type": "string" }, + "tools": { "type": "array", "items": { "type": "string" } }, + "frameworks": { "type": "array", "items": { "type": "string" } }, + "languages": { "type": "array", "items": { "type": "string" } } + } + }, + "incidents": { + "type": "array", + "items": { "$ref": "#/definitions/Incident" } + } + } + }, + "Incident": { + "type": "object", + "description": "A single occurrence of a rule violation or insight.", + "required": ["ruleId", "incidentId", "location", "locationKind"], + "additionalProperties": false, + "properties": { + "ruleId": { "type": "string" }, + "incidentId": { "type": "string" }, + "location": { "type": "string" }, + "locationKind": { "type": "string" }, + "line": { "type": "integer", "minimum": 1 }, + "column": { "type": "integer", "minimum": 1 }, + "message": { "type": "string" }, + "snippet": { "type": "string" }, + "targets": { + "type": "object", + "additionalProperties": { "$ref": "#/definitions/TargetOverride" } + }, + "labels": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "Rule": { + "type": "object", + "required": ["id", "title", "severity", "effort", "domain", "category"], + "additionalProperties": false, + "properties": { + "id": { "type": "string" }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "severity": { "$ref": "#/definitions/Severity" }, + "effort": { "type": "integer", "minimum": 0 }, + "domain": { + "type": "string", + "enum": ["cloud-readiness", "java-upgrade", "security"] + }, + "category": { "type": "string" }, + "labels": { + "type": "array", + "items": { "type": "string" } + }, + "containerization": { "type": "boolean" }, + "links": { "type": "array", "items": { "$ref": "#/definitions/Link" } } + } + }, + "SecurityFinding": { + "type": "object", + "required": ["id", "title", "category", "severity", "description", "evidence"], + "additionalProperties": false, + "properties": { + "id": { "type": "string" }, + "title": { "type": "string" }, + "category": { "type": "string" }, + "severity": { "type": "string", "enum": ["mandatory", "potential", "optional"] }, + "description": { "type": "string" }, + "storyPoint": { "type": "integer", "minimum": 0 }, + "evidence": { + "type": "object", + "required": ["files", "explanation"], + "additionalProperties": false, + "properties": { + "files": { "type": "array", "items": { "type": "string" } }, + "explanation": { "type": "string" } + } + } + } + } + } +} \ No newline at end of file diff --git a/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/report_tools.ps1 b/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/report_tools.ps1 new file mode 100644 index 0000000..ff120f0 --- /dev/null +++ b/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/report_tools.ps1 @@ -0,0 +1,494 @@ +#!/usr/bin/env pwsh +# +# Helper tools for the `assessment-report-converter` skill (PowerShell). +# +# This skill is LLM-driven: the agent reads the CSV, understands its columns, +# classifies every row, and authors report.json by hand against the schema. This +# script does NOT parse CSVs and makes NO assumptions about CSV columns. It only +# provides the deterministic lookups and validation the agent needs: +# +# list-solutions [--query KW] [--type Formula|Chat] [--ids-only] +# Print known migration solutions from solution-mapping.json, optionally +# filtered by a keyword/type. Use this to pick the right migration +# solution for an ordinary issue. +# +# rules-for-solution SOLUTION_ID +# Print the ruleId(s) mapped to a solutionId (with sourceCategory). Put a +# returned ruleId on the incident and in rules{} so the solution resolves +# downstream. An empty list means the solution has no rule. +# +# upgrade-solutions +# Print the canonical major-component upgrade solutions (jdk / spring-boot +# / spring-framework / jakarta-ee), each resolved to its ruleId. +# +# validate REPORT_JSON [--schema PATH] +# Validate a finished report.json. Runs structural + cross-field +# consistency checks (required fields, enums, every incident.ruleId exists +# in rules{}, security findings unique by id, domain/security consistency). +# --schema is accepted for CLI compatibility but ignored — these checks +# are self-contained and do not load an external JSON Schema. +# +# This is a PowerShell port of report_tools.sh; the two must stay behaviourally +# identical (same output data, same exit codes: 0 valid, 1 invalid, 2 error). + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$MappingPath = Join-Path $ScriptDir 'solution-mapping.json' + +# Enums — mirror the `enum` arrays in assessment-report.schema.json. The schema +# is the source of truth; the skill tests fail if the two drift apart. +$RULE_SEVERITY_ENUM = @('mandatory', 'potential', 'optional', 'information') +$SECURITY_SEVERITY_ENUM = @('mandatory', 'potential', 'optional') +$STATUS_ENUM = @('pending', 'running', 'completed', 'failed', 'cancelled') +$MODE_ENUM = @('issue-only', 'full') +$DOMAIN_ENUM = @('cloud-readiness', 'java-upgrade', 'security') + +function Die([string]$msg) { + [Console]::Error.WriteLine("ERROR: $msg") + exit 2 +} + +function Read-Mapping { + if (-not (Test-Path -LiteralPath $MappingPath)) { + Die "solution-mapping.json not found next to this script ($MappingPath)." + } + try { + return (Get-Content -LiteralPath $MappingPath -Raw -Encoding UTF8 | ConvertFrom-Json) + } catch { + Die "solution-mapping.json is not valid JSON ($MappingPath)." + } +} + +function Test-IsObject($v) { $v -is [System.Management.Automation.PSCustomObject] } +function Test-IsArray($v) { ($v -is [System.Array]) -or ($v -is [System.Collections.ArrayList]) } +function Test-IsNumber($v) { + ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal] -or $v -is [single]) -and ($v -isnot [bool]) +} +function Test-Uint($v) { (Test-IsNumber $v) -and ([double]$v -eq [math]::Floor([double]$v)) -and ([double]$v -ge 0) } +function Test-PosInt($v) { (Test-IsNumber $v) -and ([double]$v -eq [math]::Floor([double]$v)) -and ([double]$v -ge 1) } + +function Has($obj, [string]$key) { + if (-not (Test-IsObject $obj)) { return $false } + return ($null -ne $obj.PSObject.Properties[$key]) +} +function Get-Prop($obj, [string]$key) { + if (-not (Has $obj $key)) { return $null } + $val = $obj.$key + # Wrap only arrays with the unary comma so the pipeline doesn't unroll them + # (a single-element array would otherwise collapse to a scalar). Scalars are + # returned as-is so fields like ruleId/sourceCategory keep their scalar shape + # and string casts/comparisons behave the same as the bash implementation. + if (Test-IsArray $val) { return ,$val } + return $val +} + +# jq `tojson` for a scalar used inside a message (strings become double-quoted, +# $null becomes null, numbers/bools render bare). +function Fmt($v) { + if ($null -eq $v) { return 'null' } + if ($v -is [bool]) { if ($v) { return 'true' } else { return 'false' } } + if ($v -is [string]) { return ($v | ConvertTo-Json -Compress) } + if (Test-IsNumber $v) { return ([string]$v) } + return ($v | ConvertTo-Json -Compress -Depth 20) +} + +# Render an array (possibly empty / single element) as a JSON array string. +function ConvertTo-JsonArray($arr) { + $items = New-Object System.Collections.Generic.List[object] + if ($null -ne $arr) { foreach ($x in $arr) { $items.Add($x) } } + if ($items.Count -eq 0) { return '[]' } + return ($items.ToArray() | ConvertTo-Json -Depth 20 -AsArray) +} + +# --------------------------------------------------------------------------- # +# Shared helper: ordered {ruleId, sourceCategory} list for a solution id. +# --------------------------------------------------------------------------- # +function Get-RulesForSolution($mapping, [string]$solutionId) { + $out = New-Object System.Collections.Generic.List[object] + $rules = Get-Prop $mapping 'rules' + if (Test-IsArray $rules) { + foreach ($entry in $rules) { + if ((Get-Prop $entry 'solution') -eq $solutionId) { + $out.Add([ordered]@{ + ruleId = (Get-Prop $entry 'ruleId') + sourceCategory = (Get-Prop $entry 'sourceCategory') + }) + } + } + } + return $out +} + +# --------------------------------------------------------------------------- # +# Subcommand: list-solutions +# --------------------------------------------------------------------------- # +function Invoke-ListSolutions([string[]]$rest) { + $query = '' + $typeFilter = '' + $idsOnly = $false + for ($i = 0; $i -lt $rest.Count; $i++) { + switch -Wildcard ($rest[$i]) { + '--query' { $query = $rest[++$i]; break } + '--query=*' { $query = $rest[$i].Substring(8); break } + '--type' { $typeFilter = $rest[++$i]; break } + '--type=*' { $typeFilter = $rest[$i].Substring(7); break } + '--ids-only' { $idsOnly = $true; break } + default { Die "list-solutions: unexpected argument '$($rest[$i])'" } + } + } + $mapping = Read-Mapping + $solutions = Get-Prop $mapping 'solutions' + if (-not (Test-IsArray $solutions)) { $solutions = @() } + + $q = ($query).Trim().ToLowerInvariant() + $t = ($typeFilter).Trim().ToLowerInvariant() + + $selected = New-Object System.Collections.Generic.List[object] + foreach ($sol in $solutions) { + if ($t -ne '') { + $solType = ([string](Get-Prop $sol 'type')).ToLowerInvariant() + if ($solType -ne $t) { continue } + } + if ($q -ne '') { + $parts = @('solutionId', 'name', 'tooltip') | ForEach-Object { [string](Get-Prop $sol $_) } + $haystack = ($parts -join ' ').ToLowerInvariant() + if (-not $haystack.Contains($q)) { continue } + } + $selected.Add($sol) + } + + if ($idsOnly) { + foreach ($sol in $selected) { [Console]::Out.WriteLine([string](Get-Prop $sol 'solutionId')) } + return 0 + } + + [Console]::Out.WriteLine((ConvertTo-JsonArray $selected)) + if ($q -ne '') { + [Console]::Error.WriteLine("# $($selected.Count) solution(s) matching $(Fmt $query)") + } else { + [Console]::Error.WriteLine("# $($selected.Count) solution(s)") + } + return 0 +} + +# --------------------------------------------------------------------------- # +# Subcommand: rules-for-solution +# --------------------------------------------------------------------------- # +function Invoke-RulesForSolution([string[]]$rest) { + if ($rest.Count -lt 1) { Die 'rules-for-solution: SOLUTION_ID is required.' } + $solutionId = $rest[0] + $mapping = Read-Mapping + $rules = @(Get-RulesForSolution $mapping $solutionId) + $preferred = $null + if ($rules.Count -gt 0) { $preferred = $rules[0].ruleId } + + $result = [ordered]@{ + solutionId = $solutionId + ruleCount = $rules.Count + rules = @($rules) + preferredRuleId = $preferred + } + [Console]::Out.WriteLine(($result | ConvertTo-Json -Depth 20)) + if ($rules.Count -eq 0) { + [Console]::Error.WriteLine("# no rule maps to $(Fmt $solutionId) (likely a security-only solution; do not invent a ruleId)") + } + return 0 +} + +# --------------------------------------------------------------------------- # +# Subcommand: upgrade-solutions +# --------------------------------------------------------------------------- # +function Invoke-UpgradeSolutions([string[]]$rest) { + $components = [ordered]@{ + 'jdk' = @{ solution = 'java-version-upgrade'; label = 'Java runtime (JDK / Java SE)'; fallback = 'azure-java-version-01000' } + 'spring-boot' = @{ solution = 'spring-boot-upgrade'; label = 'Spring Boot'; fallback = 'spring-boot-to-azure-spring-boot-version-01000' } + 'spring-framework' = @{ solution = 'spring-framework-upgrade'; label = 'Spring Framework'; fallback = 'spring-framework-version-01000' } + 'jakarta-ee' = @{ solution = 'jakarta-ee-upgrade'; label = 'Java EE / Jakarta EE'; fallback = 'jakarta-ee-version-01000' } + } + + $mapping = $null + if (Test-Path -LiteralPath $MappingPath) { + try { $mapping = Get-Content -LiteralPath $MappingPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { $mapping = $null } + } + + $out = [ordered]@{} + foreach ($component in $components.Keys) { + $spec = $components[$component] + $sid = $spec.solution + $ruleIds = @() + if ($null -ne $mapping) { + $ruleIds = @(Get-RulesForSolution $mapping $sid | ForEach-Object { $_.ruleId }) + } + $preferred = if ($ruleIds.Count -gt 0) { $ruleIds[0] } else { $spec.fallback } + $out[$component] = [ordered]@{ + label = $spec.label + solutionId = $sid + ruleIds = $ruleIds + preferredRuleId = $preferred + } + } + [Console]::Out.WriteLine(($out | ConvertTo-Json -Depth 20)) + return 0 +} + +# --------------------------------------------------------------------------- # +# Subcommand: validate (structural + cross-field checks only) +# --------------------------------------------------------------------------- # +function Get-StructuralErrors($report) { + $errors = New-Object System.Collections.Generic.List[string] + + function AddMissing($obj, [string[]]$keys, [string]$where) { + if (-not (Test-IsObject $obj)) { $errors.Add("${where}: expected an object"); return } + foreach ($k in $keys) { + if (-not (Has $obj $k)) { $errors.Add("${where}: missing required field $(Fmt $k)") } + } + } + + if (-not (Test-IsObject $report)) { + $errors.Add('report: expected a JSON object') + return ,$errors + } + + # ---- report required ---- + AddMissing $report @('version', 'producer', 'metadata', 'projects', 'rules') 'report' + + # ---- metadata ---- + $meta = Get-Prop $report 'metadata' + if (-not (Test-IsObject $meta)) { + $errors.Add('metadata: expected an object') + } else { + AddMissing $meta @('id', 'name', 'status', 'analysisStartTime', 'domains', 'targetIds') 'metadata' + if (Has $meta 'status') { + $status = Get-Prop $meta 'status' + if ($STATUS_ENUM -notcontains $status) { + $errors.Add("metadata.status: invalid value $(Fmt $status) (must be one of $($STATUS_ENUM -join ' | '))") + } + } + if (Has $meta 'mode') { + $mode = Get-Prop $meta 'mode' + if ($MODE_ENUM -notcontains $mode) { + $errors.Add("metadata.mode: invalid value $(Fmt $mode) (must be one of $($MODE_ENUM -join ' | '))") + } + } + if (Has $meta 'domains') { + $domains = Get-Prop $meta 'domains' + if (-not (Test-IsArray $domains)) { + $errors.Add('metadata.domains: expected an array') + } else { + foreach ($d in $domains) { + if ($DOMAIN_ENUM -notcontains $d) { + $errors.Add("metadata.domains: invalid value $(Fmt $d) (must be one of $($DOMAIN_ENUM -join ' | '))") + } + } + } + } + } + + # ---- rules (object keyed by ruleId) ---- + $ruleIds = @() + $rules = Get-Prop $report 'rules' + if (-not (Test-IsObject $rules)) { + $errors.Add('rules: expected an object keyed by ruleId') + } else { + foreach ($prop in $rules.PSObject.Properties) { + $rid = $prop.Name + $rule = $prop.Value + $ruleIds += $rid + AddMissing $rule @('id', 'title', 'severity', 'effort', 'domain', 'category') "rules[$rid]" + # Run the value checks independently of missing-field checks (guarded by + # Has), so a rule that is both missing a field AND carries an invalid + # value reports both — matching report_tools.sh, which concatenates. + if (Test-IsObject $rule) { + if ((Has $rule 'severity') -and ($RULE_SEVERITY_ENUM -notcontains (Get-Prop $rule 'severity'))) { + $errors.Add("rules[$rid].severity: invalid value $(Fmt (Get-Prop $rule 'severity')) (must be one of $($RULE_SEVERITY_ENUM -join ' | '))") + } + if ((Has $rule 'effort') -and (-not (Test-Uint (Get-Prop $rule 'effort')))) { + $errors.Add("rules[$rid].effort: must be an integer >= 0") + } + if ((Has $rule 'domain') -and ($DOMAIN_ENUM -notcontains (Get-Prop $rule 'domain'))) { + $errors.Add("rules[$rid].domain: invalid value $(Fmt (Get-Prop $rule 'domain')) (must be one of $($DOMAIN_ENUM -join ' | '))") + } + } + } + } + + # ---- projects + incidents ---- + $projects = Get-Prop $report 'projects' + if (-not (Test-IsArray $projects)) { + $errors.Add('projects: expected an array') + } else { + for ($pi = 0; $pi -lt @($projects).Count; $pi++) { + $project = @($projects)[$pi] + $pw = "projects[$pi]" + AddMissing $project @('path', 'properties', 'incidents') $pw + if (-not (Test-IsObject $project)) { continue } + + $props = Get-Prop $project 'properties' + if ($null -eq $props) { $props = [PSCustomObject]@{} } + AddMissing $props @('appName') "$pw.properties" + + $incidents = Get-Prop $project 'incidents' + if ((Has $project 'incidents') -and (-not (Test-IsArray $incidents))) { + $errors.Add("$pw.incidents: expected an array") + } elseif (Test-IsArray $incidents) { + for ($ii = 0; $ii -lt @($incidents).Count; $ii++) { + $inc = @($incidents)[$ii] + $iw = "$pw.incidents[$ii]" + AddMissing $inc @('ruleId', 'incidentId', 'location', 'locationKind') $iw + if (Test-IsObject $inc) { + if ((Has $inc 'ruleId') -and ($ruleIds -notcontains (Get-Prop $inc 'ruleId'))) { + $errors.Add("$iw.ruleId $(Fmt (Get-Prop $inc 'ruleId')) has no matching entry in rules{}") + } + if ((Has $inc 'line') -and (-not (Test-PosInt (Get-Prop $inc 'line')))) { + $errors.Add("$iw.line: must be an integer >= 1") + } + if ((Has $inc 'column') -and (-not (Test-PosInt (Get-Prop $inc 'column')))) { + $errors.Add("$iw.column: must be an integer >= 1") + } + } + } + } + } + } + + # ---- security findings ---- + $security = Get-Prop $report 'security' + if ($null -eq $security) { $security = @() } + if (-not (Test-IsArray $security)) { + $errors.Add('security: expected an array') + $security = @() + } + $ids = New-Object System.Collections.Generic.List[object] + for ($si = 0; $si -lt @($security).Count; $si++) { + $finding = @($security)[$si] + $sw = "security[$si]" + AddMissing $finding @('id', 'title', 'category', 'severity', 'description', 'evidence') $sw + if (Test-IsObject $finding) { + # Collect every object finding's id (even ones missing other fields) so + # duplicate detection matches report_tools.sh's group_by(.id). + $ids.Add((Get-Prop $finding 'id')) + if ((Has $finding 'severity') -and ($SECURITY_SEVERITY_ENUM -notcontains (Get-Prop $finding 'severity'))) { + $errors.Add("$sw.severity: invalid value $(Fmt (Get-Prop $finding 'severity')) (must be one of $($SECURITY_SEVERITY_ENUM -join ' | ') — normalize the source CVE/CWE severity)") + } + # Mirror bash `($finding.evidence // {})`: a missing evidence defaults to + # {} so its own required sub-fields are reported, rather than only + # "expected an object". + $ev = Get-Prop $finding 'evidence' + if ($null -eq $ev) { $ev = [PSCustomObject]@{} } + if (-not (Test-IsObject $ev)) { + $errors.Add("$sw.evidence: expected an object") + } else { + AddMissing $ev @('files', 'explanation') "$sw.evidence" + if ((Has $ev 'files') -and (-not (Test-IsArray (Get-Prop $ev 'files')))) { + $errors.Add("$sw.evidence.files: must be an array") + } + } + } + } + # duplicate ids (one message per duplicated id, in first-seen order) + $seen = @{} + $dupReported = @{} + foreach ($id in $ids) { + $key = if ($null -eq $id) { "`0null`0" } else { [string]$id } + if ($seen.ContainsKey($key)) { + if (-not $dupReported.ContainsKey($key)) { + $errors.Add("security id $(Fmt $id) is duplicated (merge findings by id)") + $dupReported[$key] = $true + } + } else { + $seen[$key] = $true + } + } + + # ---- domain <-> security consistency ---- + $domains2 = Get-Prop $meta 'domains' + if (-not (Test-IsArray $domains2)) { $domains2 = @() } + $secArr = Get-Prop $report 'security' + if (-not (Test-IsArray $secArr)) { $secArr = @() } + if (($domains2 -contains 'security') -and (@($secArr).Count -eq 0)) { + $errors.Add('metadata.domains includes "security" but report.security is empty') + } + if ((@($secArr).Count -gt 0) -and ($domains2 -notcontains 'security')) { + $errors.Add('report.security has findings but metadata.domains does not include "security"') + } + + return ,$errors +} + +function Invoke-Validate([string[]]$rest) { + $report = '' + $schema = '' + for ($i = 0; $i -lt $rest.Count; $i++) { + switch -Wildcard ($rest[$i]) { + '--schema' { $schema = $rest[++$i]; break } + '--schema=*' { $schema = $rest[$i].Substring(9); break } + '-*' { Die "validate: unexpected option '$($rest[$i])'" } + default { + if ($report -eq '') { $report = $rest[$i] } + else { Die "validate: unexpected argument '$($rest[$i])'" } + } + } + } + $null = $schema # accepted for compatibility; unused + if ($report -eq '') { Die 'validate: REPORT_JSON path is required.' } + if (-not (Test-Path -LiteralPath $report)) { Die "cannot read report: $report" } + try { + $data = Get-Content -LiteralPath $report -Raw -Encoding UTF8 | ConvertFrom-Json + } catch { + Die "report is not valid JSON: $report" + } + + $errors = Get-StructuralErrors $data + + [Console]::Out.WriteLine("Report: $report") + [Console]::Out.WriteLine(('-' * 60)) + if ($errors.Count -eq 0) { + [Console]::Out.WriteLine('structural + consistency checks: OK') + } else { + [Console]::Out.WriteLine("structural + consistency checks: $($errors.Count) error(s)") + foreach ($line in $errors) { [Console]::Out.WriteLine(" - $line") } + } + [Console]::Out.WriteLine(('-' * 60)) + if ($errors.Count -eq 0) { + [Console]::Out.WriteLine('RESULT: VALID') + return 0 + } else { + [Console]::Out.WriteLine('RESULT: INVALID') + return 1 + } +} + +# --------------------------------------------------------------------------- # +# CLI dispatch +# --------------------------------------------------------------------------- # +function Show-Usage { + [Console]::Error.WriteLine(@' +Usage: report_tools.ps1 [args] + +Commands: + list-solutions [--query KW] [--type Formula|Chat] [--ids-only] + rules-for-solution SOLUTION_ID + upgrade-solutions + validate REPORT_JSON [--schema PATH] +'@) + exit 2 +} + +$argv = @($args) +if ($argv.Count -lt 1) { Show-Usage } +$command = $argv[0] +$rest = @() +if ($argv.Count -gt 1) { $rest = $argv[1..($argv.Count - 1)] } + +switch ($command) { + 'list-solutions' { exit (Invoke-ListSolutions $rest) } + 'rules-for-solution' { exit (Invoke-RulesForSolution $rest) } + 'upgrade-solutions' { exit (Invoke-UpgradeSolutions $rest) } + 'validate' { exit (Invoke-Validate $rest) } + '-h' { Show-Usage } + '--help' { Show-Usage } + 'help' { Show-Usage } + default { Die "unknown command '$command' (see --help)" } +} diff --git a/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/report_tools.sh b/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/report_tools.sh new file mode 100644 index 0000000..09c282d --- /dev/null +++ b/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/report_tools.sh @@ -0,0 +1,349 @@ +#!/usr/bin/env bash +# +# Helper tools for the `assessment-report-converter` skill (bash + jq). +# +# This skill is LLM-driven: the agent reads the CSV, understands its columns, +# classifies every row, and authors report.json by hand against the schema. This +# script does NOT parse CSVs and makes NO assumptions about CSV columns. It only +# provides the deterministic lookups and validation the agent needs: +# +# list-solutions [--query KW] [--type Formula|Chat] [--ids-only] +# Print known migration solutions from solution-mapping.json, optionally +# filtered by a keyword/type. Use this to pick the right migration +# solution for an ordinary issue. +# +# rules-for-solution SOLUTION_ID +# Print the ruleId(s) mapped to a solutionId (with sourceCategory). Put a +# returned ruleId on the incident and in rules{} so the solution resolves +# downstream. An empty list means the solution has no rule. +# +# upgrade-solutions +# Print the canonical major-component upgrade solutions (jdk / spring-boot +# / spring-framework / jakarta-ee), each resolved to its ruleId. +# +# validate REPORT_JSON [--schema PATH] +# Validate a finished report.json. Runs structural + cross-field +# consistency checks (required fields, enums, every incident.ruleId exists +# in rules{}, security findings unique by id, domain/security consistency). +# --schema is accepted for CLI compatibility but ignored — these checks +# are self-contained and do not load an external JSON Schema. +# +# Requires: bash + jq. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MAPPING_PATH="$SCRIPT_DIR/solution-mapping.json" + +# Enums — mirror the `enum` arrays in assessment-report.schema.json. The schema +# is the source of truth; the skill tests fail if the two drift apart. +RULE_SEVERITY_ENUM='["mandatory","potential","optional","information"]' +SECURITY_SEVERITY_ENUM='["mandatory","potential","optional"]' +STATUS_ENUM='["pending","running","completed","failed","cancelled"]' +MODE_ENUM='["issue-only","full"]' +DOMAIN_ENUM='["cloud-readiness","java-upgrade","security"]' + +die() { echo "ERROR: $*" >&2; exit 2; } + +require_jq() { + command -v jq >/dev/null 2>&1 || die "jq is required but was not found on PATH (install jq)." +} + +load_mapping() { + [ -f "$MAPPING_PATH" ] || die "solution-mapping.json not found next to this script ($MAPPING_PATH)." +} + +hr() { printf '%.0s-' $(seq 1 60); echo; } + +# --------------------------------------------------------------------------- # +# Subcommand: list-solutions +# --------------------------------------------------------------------------- # +cmd_list_solutions() { + local query="" type_filter="" ids_only=0 + while [ $# -gt 0 ]; do + case "$1" in + --query) query="${2:-}"; shift 2 ;; + --query=*) query="${1#*=}"; shift ;; + --type) type_filter="${2:-}"; shift 2 ;; + --type=*) type_filter="${1#*=}"; shift ;; + --ids-only) ids_only=1; shift ;; + *) die "list-solutions: unexpected argument '$1'" ;; + esac + done + load_mapping + + local q; q="$(printf '%s' "$query" | tr '[:upper:]' '[:lower:]' | sed -e 's/^ *//' -e 's/ *$//')" + local t; t="$(printf '%s' "$type_filter" | tr '[:upper:]' '[:lower:]' | sed -e 's/^ *//' -e 's/ *$//')" + + local selected + selected="$(jq --arg q "$q" --arg t "$t" ' + (.solutions // []) + | map(select( + (($t == "") or ((.type // "" | ascii_downcase) == $t)) + and + (($q == "") or + (([.solutionId, .name, .tooltip] | map(. // "" | tostring) | join(" ") | ascii_downcase) | contains($q))) + )) + ' "$MAPPING_PATH")" + + if [ "$ids_only" -eq 1 ]; then + printf '%s' "$selected" | jq -r '.[].solutionId // ""' + return 0 + fi + + printf '%s\n' "$selected" | jq '.' + local count; count="$(printf '%s' "$selected" | jq 'length')" + if [ -n "$q" ]; then + printf '# %s solution(s) matching "%s"\n' "$count" "$query" >&2 + else + printf '# %s solution(s)\n' "$count" >&2 + fi +} + +# --------------------------------------------------------------------------- # +# Subcommand: rules-for-solution +# --------------------------------------------------------------------------- # +cmd_rules_for_solution() { + [ $# -ge 1 ] || die "rules-for-solution: SOLUTION_ID is required." + local solution_id="$1" + load_mapping + + jq --arg sid "$solution_id" ' + [ (.rules // [])[] | select(.solution == $sid) | {ruleId: .ruleId, sourceCategory: .sourceCategory} ] as $rules + | { + solutionId: $sid, + ruleCount: ($rules | length), + rules: $rules, + preferredRuleId: ($rules[0].ruleId // null) + } + ' "$MAPPING_PATH" + + local count; count="$(jq --arg sid "$solution_id" '[ (.rules // [])[] | select(.solution == $sid) ] | length' "$MAPPING_PATH")" + if [ "$count" -eq 0 ]; then + printf '# no rule maps to "%s" (likely a security-only solution; do not invent a ruleId)\n' "$solution_id" >&2 + fi +} + +# --------------------------------------------------------------------------- # +# Subcommand: upgrade-solutions +# --------------------------------------------------------------------------- # +cmd_upgrade_solutions() { + local mapping_json="{}" + if [ -f "$MAPPING_PATH" ]; then + mapping_json="$(cat "$MAPPING_PATH")" + fi + + jq -n --argjson mapping "$mapping_json" ' + def rules_for($sid): [ ($mapping.rules // [])[] | select(.solution == $sid) | .ruleId ]; + { + "jdk": {solution: "java-version-upgrade", label: "Java runtime (JDK / Java SE)", fallback: "azure-java-version-01000"}, + "spring-boot": {solution: "spring-boot-upgrade", label: "Spring Boot", fallback: "spring-boot-to-azure-spring-boot-version-01000"}, + "spring-framework": {solution: "spring-framework-upgrade", label: "Spring Framework", fallback: "spring-framework-version-01000"}, + "jakarta-ee": {solution: "jakarta-ee-upgrade", label: "Java EE / Jakarta EE", fallback: "jakarta-ee-version-01000"} + } + | to_entries + | map( + .value.solution as $sid + | rules_for($sid) as $ids + | { + key: .key, + value: { + label: .value.label, + solutionId: $sid, + ruleIds: $ids, + preferredRuleId: ($ids[0] // .value.fallback) + } + } + ) + | from_entries + ' +} + +# --------------------------------------------------------------------------- # +# Subcommand: validate (structural + cross-field checks only) +# --------------------------------------------------------------------------- # +cmd_validate() { + local report="" schema="" + while [ $# -gt 0 ]; do + case "$1" in + --schema) schema="${2:-}"; shift 2 ;; + --schema=*) schema="${1#*=}"; shift ;; + -*) die "validate: unexpected option '$1'" ;; + *) if [ -z "$report" ]; then report="$1"; shift; else die "validate: unexpected argument '$1'"; fi ;; + esac + done + : "${schema:-}" # accepted for compatibility; unused + [ -n "$report" ] || die "validate: REPORT_JSON path is required." + [ -f "$report" ] || die "cannot read report: $report" + jq empty "$report" >/dev/null 2>&1 || die "report is not valid JSON: $report" + + local errors + errors="$(jq -r \ + --argjson sev "$RULE_SEVERITY_ENUM" \ + --argjson ssev "$SECURITY_SEVERITY_ENUM" \ + --argjson status "$STATUS_ENUM" \ + --argjson mode "$MODE_ENUM" \ + --argjson domain "$DOMAIN_ENUM" ' + def missing($obj; $keys; $where): + if ($obj | type) != "object" then ["\($where): expected an object"] + else [ $keys[] as $k | select(($obj | has($k)) | not) | "\($where): missing required field \($k|tojson)" ] + end; + def is_uint($v): ($v | type) == "number" and ($v == ($v | floor)) and ($v >= 0); + def is_pos_int($v): ($v | type) == "number" and ($v == ($v | floor)) and ($v >= 1); + + . as $r + | if ($r | type) != "object" then ["report: expected a JSON object"] + else + # ---- report required ---- + missing($r; ["version","producer","metadata","projects","rules"]; "report") + + + # ---- metadata ---- + ( ($r.metadata) as $meta + | if ($meta | type) != "object" then ["metadata: expected an object"] + else + missing($meta; ["id","name","status","analysisStartTime","domains","targetIds"]; "metadata") + + (if ($meta | has("status")) and (($status | index($meta.status)) == null) + then ["metadata.status: invalid value \($meta.status|tojson) (must be one of \($status|join(" | ")))"] else [] end) + + (if ($meta | has("mode")) and (($mode | index($meta.mode)) == null) + then ["metadata.mode: invalid value \($meta.mode|tojson) (must be one of \($mode|join(" | ")))"] else [] end) + + (if ($meta | has("domains")) and (($meta.domains | type) != "array") + then ["metadata.domains: expected an array"] + elif ($meta | has("domains")) + then [ $meta.domains[] as $d | select(($domain | index($d)) == null) | "metadata.domains: invalid value \($d|tojson) (must be one of \($domain|join(" | ")))" ] + else [] end) + end ) + + + # ---- rules ---- + ( ($r.rules) as $rules + | if ($rules | type) != "object" then ["rules: expected an object keyed by ruleId"] + else + [ $rules | to_entries[] + | .key as $rid | .value as $rule + | (missing($rule; ["id","title","severity","effort","domain","category"]; "rules[\($rid)]") + + (if ($rule | type) == "object" then + (if ($rule | has("severity")) and (($sev | index($rule.severity)) == null) + then ["rules[\($rid)].severity: invalid value \($rule.severity|tojson) (must be one of \($sev|join(" | ")))"] else [] end) + + (if ($rule | has("effort")) and (is_uint($rule.effort) | not) + then ["rules[\($rid)].effort: must be an integer >= 0"] else [] end) + + (if ($rule | has("domain")) and (($domain | index($rule.domain)) == null) + then ["rules[\($rid)].domain: invalid value \($rule.domain|tojson) (must be one of \($domain|join(" | ")))"] else [] end) + else [] end)) + ] | add // [] + end ) + + + # ---- projects + incidents ---- + ( ($r.rules // {} | keys) as $ruleIds + | ($r.projects) as $projects + | if ($projects | type) != "array" then ["projects: expected an array"] + else + [ $projects | to_entries[] + | .key as $pi | .value as $project + | "projects[\($pi)]" as $pw + | (missing($project; ["path","properties","incidents"]; $pw) + + (if ($project | type) == "object" then + (missing(($project.properties // {}); ["appName"]; "\($pw).properties")) + + (if ($project | has("incidents")) and (($project.incidents | type) != "array") + then ["\($pw).incidents: expected an array"] + elif (($project.incidents // []) | type) == "array" then + [ ($project.incidents // []) | to_entries[] + | .key as $ii | .value as $inc + | "\($pw).incidents[\($ii)]" as $iw + | (missing($inc; ["ruleId","incidentId","location","locationKind"]; $iw) + + (if ($inc | type) == "object" then + (if ($inc | has("ruleId")) and (($ruleIds | index($inc.ruleId)) == null) + then ["\($iw).ruleId \($inc.ruleId|tojson) has no matching entry in rules{}"] else [] end) + + (if ($inc | has("line")) and (is_pos_int($inc.line) | not) then ["\($iw).line: must be an integer >= 1"] else [] end) + + (if ($inc | has("column")) and (is_pos_int($inc.column) | not) then ["\($iw).column: must be an integer >= 1"] else [] end) + else [] end)) + ] | add // [] + else [] end) + else [] end)) + ] | add // [] + end ) + + + # ---- security findings ---- + ( ($r.security // []) as $security0 + | if ($security0 | type) != "array" then ["security: expected an array"] + else + ([ $security0 | to_entries[] + | .key as $si | .value as $finding + | "security[\($si)]" as $sw + | (missing($finding; ["id","title","category","severity","description","evidence"]; $sw) + + (if ($finding | type) == "object" then + (if ($finding | has("severity")) and (($ssev | index($finding.severity)) == null) + then ["\($sw).severity: invalid value \($finding.severity|tojson) (must be one of \($ssev|join(" | ")) — normalize the source CVE/CWE severity)"] else [] end) + + (($finding.evidence // {}) as $ev + | if ($ev | type) != "object" then ["\($sw).evidence: expected an object"] + else missing($ev; ["files","explanation"]; "\($sw).evidence") + + (if ($ev | has("files")) and (($ev.files | type) != "array") then ["\($sw).evidence.files: must be an array"] else [] end) + end) + else [] end)) + ] | add // []) + + ( [ $security0[] | select(type=="object") ] + | group_by(.id) | [ .[] | select(length > 1) | .[0].id ] + | map("security id \(.|tojson) is duplicated (merge findings by id)") ) + end ) + + + # ---- domain <-> security consistency ---- + ( ($r.metadata.domains // []) as $domains + | ($r.security // []) as $sec + | (if (($domains | type) == "array") and ($domains | index("security")) and (($sec|length) == 0) + then ["metadata.domains includes \"security\" but report.security is empty"] else [] end) + + (if (($sec|length) > 0) and (($domains | index("security")) == null) + then ["report.security has findings but metadata.domains does not include \"security\""] else [] end) ) + end + | .[] + ' "$report")" + + echo "Report: $report" + hr + if [ -z "$errors" ]; then + echo "structural + consistency checks: OK" + else + local n; n="$(printf '%s\n' "$errors" | grep -c .)" + echo "structural + consistency checks: $n error(s)" + printf '%s\n' "$errors" | while IFS= read -r line; do + [ -n "$line" ] && echo " - $line" + done + fi + hr + if [ -z "$errors" ]; then + echo "RESULT: VALID" + return 0 + else + echo "RESULT: INVALID" + return 1 + fi +} + +# --------------------------------------------------------------------------- # +# CLI dispatch +# --------------------------------------------------------------------------- # +usage() { + cat >&2 <<'EOF' +Usage: report_tools.sh [args] + +Commands: + list-solutions [--query KW] [--type Formula|Chat] [--ids-only] + rules-for-solution SOLUTION_ID + upgrade-solutions + validate REPORT_JSON [--schema PATH] +EOF + exit 2 +} + +main() { + require_jq + [ $# -ge 1 ] || usage + local command="$1"; shift + case "$command" in + list-solutions) cmd_list_solutions "$@" ;; + rules-for-solution) cmd_rules_for_solution "$@" ;; + upgrade-solutions) cmd_upgrade_solutions "$@" ;; + validate) cmd_validate "$@" ;; + -h|--help|help) usage ;; + *) die "unknown command '$command' (see --help)" ;; + esac +} + +main "$@" diff --git a/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/solution-mapping-schema.json b/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/solution-mapping-schema.json new file mode 100644 index 0000000..c3c3cd4 --- /dev/null +++ b/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/solution-mapping-schema.json @@ -0,0 +1,362 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "solutions", + "rules" + ], + "definitions": { + "solutionId": { + "type": "string", + "enum": [ + "scan-and-resolve-cwe-vulnerabilities", + "scan-and-resolve-cve-vulnerabilities", + "activemq-servicebus", + "amqp-rabbitmq-servicebus", + "java-ee-amqp-rabbitmq-servicebus", + "ibm-mq-jms-to-azure-service-bus", + "javax.email-send-to-azure-communication-service-email", + "jax-rpc-to-jax-ws", + "java-version-upgrade", + "deprecated-api-upgrade", + "spring-framework-upgrade", + "spring-boot-upgrade", + "jakarta-ee-upgrade", + "confluent-cloud-kafka", + "kafka-to-eventhubs", + "other-cache-solutions-to-azure-managed-cache", + "log-to-console", + "mi-azure-sql", + "mi-cassandra", + "mi-mariadb", + "mi-mongodb", + "mi-mysql", + "mi-postgresql", + "AWS-secrets-manager-to-azure-key-vault", + "certificate-management-to-azure-key-vault", + "local-files-to-mounted-azure-storage", + "on-premises-user-authentication-to-microsoft-entra-id", + "plaintext-credential-to-azure-keyvault", + "s3-to-azure-blob-storage", + "spring-jms-rabbitmq-servicebus", + "sqs-to-servicebus", + "bare/redesign-java-gui-app", + "bare/apm-to-application-insights", + "bare/encoding-standards", + "bare/local-resource-access", + "bare/remote-communication", + "bare/remote-communication/java-socket", + "bare/remote-communication/corba", + "bare/remote-communication/hardcode-ip", + "bare/remote-communication/secure-protocols", + "bare/remote-communication/hardcoded-urls", + "bare/appserver-api-migration-to-standard-java", + "bare/os-compatibility", + "bare/java-native-code", + "bare/jakataee-to-azure", + "bare/jakataee-to-azure/rmi", + "bare/jakataee-to-azure/jca", + "bare/configuration-management/environment-variables", + "bare/configuration-management/external-configuration", + "bare/configuration-management/windows-registry", + "bare/spring-migration", + "bare/eap-migration/jboss-eap", + "bare/azure-service-connector", + "bare/aws-region-configuration-to-azure", + "bare/spring-cloud-vault-migration", + "bare/aws-credentials-to-azure", + "bare/openliberty-migration/openliberty-database", + "bare/openliberty-migration/openliberty-filesystem", + "bare/openliberty-migration/openliberty-jms", + "bare/openliberty-migration/openliberty-logging", + "bare/database-migration/database-reliability", + "bare/oraclejdk-to-openjdk/resource-management-apis", + "bare/oraclejdk-to-openjdk/imageio", + "bare/jakarta-auth-migration", + "bare/jakarta-websocket-migration", + "bare/jakarta-jaxrs-migration", + "bare/jakarta-nosql-migration", + "bare/jakarta-persistence-migration", + "bare/jakarta-data-migration", + "bare/jboss-eap-to-azure-app-service", + "bare/jboss-eap-to-aks", + "bare/jboss-eap-to-azure-container-apps", + "bare/weblogic-to-azure-app-service", + "bare/weblogic-to-aks", + "bare/weblogic-to-azure-container-apps", + "bare/websphere-to-azure-app-service", + "bare/websphere-to-aks", + "bare/websphere-to-azure-container-apps", + "azure-legacy-java-sdk-upgrade", + "oracle-to-postgresql", + "eclipse-project-to-maven-project", + "ant-project-to-maven-project", + "containerization-copilot-agent", + "google-gcr-to-azure-acr", + "spring-cloud-config-to-azure-app-configuration", + "sybase-ase-to-azure-postgresql", + "sybase-ase-to-azure-sql-database", + "google-firestore-to-azure-cosmos-db", + "google-cloud-bigtable-to-azure-cosmos-db", + "google-cloud-spanner-to-azure-postgresql", + "apache-pulsar-to-azure-event-hubs", + "ibm-db2-to-azure-postgresql", + "firebird-to-azure-postgresql", + "sqlite-to-azure-postgresql", + "google-cloud-functions-to-azure-functions", + "aws-lambda-to-azure-functions", + "quartz-scheduler-to-azure-functions", + "spring-batch-to-azure-durable-functions", + "google-cloud-storage-to-azure-blob-storage", + "amazon-sns-to-azure-service-bus", + "tibco-ems-jms-to-azure-service-bus", + "solace-pubsub-to-azure-service-bus", + "amazon-kinesis-to-azure-event-hubs", + "google-cloud-pub-sub-to-azure-service-bus", + "bare/aws-bedrock-to-azure-ai", + "bare/weak-cryptography", + "bare/insecure-tls", + "bare/hardcoded-credentials", + "bare/insecure-random", + "bare/thirdparty-generic" + ], + "description": "Canonical solution identifier used by this schema for both Formula and Chat solutions. Many IDs map to existing formula/kb folder names for compatibility, but this field should be treated as an opaque stable ID. Add new solution IDs to this enum before referencing them in rules or solution entries." + }, + "solutionEntry": { + "type": "object", + "required": [ + "solutionId", + "name", + "type", + "tooltip" + ], + "properties": { + "solutionId": { + "$ref": "#/definitions/solutionId" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "type": { + "enum": [ + "Formula", + "Chat" + ], + "description": "The type of the solution. Formula: formulas were built for this solution or a prompt is sent to the Agent mode for code migration. Chat: a prompt is sent to the Chat mode for more guidance." + }, + "description": { + "type": [ + "string" + ] + }, + "effort": { + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ], + "description": "The estimated effort to do the migration." + }, + "prompt": { + "type": [ + "string" + ], + "description": "The prompt that is sent to Copilot Chat or Agent mode for assistance." + }, + "variants": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/solutionId" + } + }, + "tooltip": { + "type": "string", + "description": "A tooltip is shown in the UI to provide more information about the solution." + }, + "experimental": { + "type": "boolean", + "description": "Whether the solution is experimental and to use the scenario name instead of kbId format when invoking this solution. When true, the solution will be invoked using the scenario name instead of 'by kbId: ' format." + } + }, + "if": { + "properties": { + "type": { + "const": "Chat" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "prompt" + ] + }, + "else": { + "not": { + "required": [ + "prompt" + ] + } + }, + "additionalProperties": false + } + }, + "properties": { + "$schema": { + "type": "string", + "description": "URI reference to the schema" + }, + "solutions": { + "type": "array", + "items": { + "$ref": "#/definitions/solutionEntry" + }, + "description": "List of solutions that are available for the rules. The solutionId is used to reference the solution in the rules." + }, + "rules": { + "type": "array", + "description": "List of rules that are used to assess the application.", + "items": { + "type": "object", + "required": [ + "ruleId" + ], + "properties": { + "ruleId": { + "type": "string", + "minLength": 1, + "description": "The rule ID in AppCat. Should be unique across all rules." + }, + "sourceCategory": { + "enum": [ + "activemq", + "activemq-artemis", + "ant", + "apm-dynatrace", + "apm-elastic", + "apm-newrelic", + "aws-credentials", + "aws-region-configuration", + "aws-s3", + "aws-secrets-manager", + "aws-sqs", + "cassandra", + "corba", + "rmi", + "jca", + "environment-variables", + "hardcode-ip", + "secure-protocols", + "hardcoded-urls", + "eclipse", + "external-configuration", + "windows-registry", + "http-session", + "jms-ibm-mq", + "java-mail", + "java-socket", + "javafx", + "javax-swing", + "jboss-eap", + "jni-native-code", + "kafka", + "local-file-system", + "localhost", + "logstash", + "mariadb", + "microsoft-sql", + "mongodb", + "mysql", + "oauth2", + "openid", + "opensaml", + "postgresql", + "quartz-scheduler", + "redis", + "saml", + "webform-auth", + "splunk", + "spring-amqp-rabbitmq", + "java-ee-amqp-rabbitmq", + "spring-cloud", + "spring-boot", + "spring-framework", + "java-ee/jakarta-ee", + "spring-cloud-vault", + "spring-jms-rabbitmq", + "spring-security", + "tanzu-application-service", + "zipkin", + "openliberty-database", + "openliberty-filesystem", + "openliberty-jms", + "openliberty-logging", + "oracle", + "google-pubsub", + "google-gcr", + "sybase-ase", + "google-firestore", + "google-cloud-bigtable", + "google-cloud-spanner", + "apache-pulsar", + "ibm-db2", + "firebird", + "sqlite", + "google-cloud-functions", + "aws-lambda", + "spring-batch", + "google-cloud-storage", + "amazon-sns", + "tibco-ems-jms", + "solace-pubsubplus", + "amazon-kinesis", + "aws-bedrock", + "jakarta-auth", + "jakarta-websocket", + "jakarta-jaxrs", + "jakarta-nosql", + "jakarta-persistence", + "jakarta-data", + "weblogic-to-azure-app-service", + "weblogic-to-aks", + "weblogic-to-azure-container-apps", + "jboss-eap-to-azure-app-service", + "jboss-eap-to-aks", + "jboss-eap-to-azure-container-apps", + "websphere-to-azure-app-service", + "websphere-to-aks", + "websphere-to-azure-container-apps" + ], + "description": "The source category of the rule. This is mainly used to determine the solution when category is not enough." + }, + "solution": { + "oneOf": [ + { + "$ref": "#/definitions/solutionId" + } + ], + "description": "The solution that is used to handle this rule." + }, + "prompt": { + "type": [ + "string" + ], + "description": "The prompt that is sent to Copilot for assistance, before we have a solution." + }, + "notes": { + "type": [ + "string" + ], + "description": "Implementation notes for the rule. Provides future direction for the rule." + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/solution-mapping.json b/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/solution-mapping.json new file mode 100644 index 0000000..7e45499 --- /dev/null +++ b/plugins/github-copilot-modernization/skills/assessment-report-converter/scripts/solution-mapping.json @@ -0,0 +1,2393 @@ +{ + "$schema": "./solution-mapping-schema.json", + "solutions": [ + { + "solutionId": "bare/thirdparty-generic", + "name": "Get migration guidance from Copilot", + "type": "Chat", + "prompt": "Analyze this migration issue in the context of the affected code and application architecture. Provide a clear explanation of the underlying problem, the Azure-ready remediation strategy, and any relevant tradeoffs or prerequisites. Then outline a practical, step-by-step implementation plan, including the code, configuration, dependency, and validation changes needed to complete the migration safely.", + "tooltip": "No specific solution matched this issue. Chat with Copilot for tailored migration guidance." + }, + { + "solutionId": "scan-and-resolve-cwe-vulnerabilities", + "name": "Scan and resolve CWE vulnerabilities", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Security vulnerability detected. Ask Copilot to scan and resolve it." + }, + { + "solutionId": "scan-and-resolve-cve-vulnerabilities", + "name": "Resolve CVE issues by upgrading to secure, vulnerability-free versions", + "type": "Formula", + "effort": "MEDIUM", + "tooltip": "Scan and fix CVE vulnerabilities." + }, + { + "solutionId": "activemq-servicebus", + "name": "Migrate from Active Artemis to Azure Service Bus", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from ActiveMQ Artemis to Azure Service Bus for messaging." + }, + { + "solutionId": "java-ee-amqp-rabbitmq-servicebus", + "name": "Migrate from RabbitMQ(AMQP) to Azure Service Bus for Java EE/Jakarta EE", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from RabbitMQ with AMQP to Azure Service Bus for messaging in Java EE/Jakarta EE applications." + }, + { + "solutionId": "amqp-rabbitmq-servicebus", + "name": "Migrate from RabbitMQ(AMQP) to Azure Service Bus", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from RabbitMQ with AMQP to Azure Service Bus for messaging." + }, + { + "solutionId": "ibm-mq-jms-to-azure-service-bus", + "name": "Migrate IBM MQ to Azure Service Bus via JMS", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from IBM JMS to Azure Service Bus for messaging.", + "experimental": true + }, + { + "solutionId": "javax.email-send-to-azure-communication-service-email", + "name": "Migrate to Azure Communication Service", + "type": "Formula", + "effort": "LOW", + "tooltip": "Migrate from Javax Email to Azure Communication Service for sending emails." + }, + { + "solutionId": "jax-rpc-to-jax-ws", + "name": "Migrate from JAX-RPC to JAX-WS", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from JAX-RPC to JAX-WS for web services. JAX-RPC is deprecated and JAX-WS is the recommended alternative." + }, + { + "solutionId": "java-version-upgrade", + "name": "Upgrade Java Version", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Upgrade to the latest stable version of Java for improved security, performance, and compatibility." + }, + { + "solutionId": "deprecated-api-upgrade", + "name": "Upgrade Deprecated APIs", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Upgrade deprecated APIs to their recommended alternatives for improved security, performance, and compatibility." + }, + { + "solutionId": "spring-boot-upgrade", + "name": "Upgrade Spring Boot Version", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Upgrade to the latest stable version of Spring Boot for improved security, performance, and compatibility." + }, + { + "solutionId": "spring-framework-upgrade", + "name": "Upgrade Spring Framework Version", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Upgrade to the latest stable version of Spring Framework for improved security, performance, and compatibility." + }, + { + "solutionId": "jakarta-ee-upgrade", + "name": "Upgrade Jakarta EE Version", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Upgrade to the latest stable version of Jakarta EE for improved security, performance, and compatibility." + }, + { + "solutionId": "confluent-cloud-kafka", + "name": "Migrate from Kafka to Kafka on Confluent Cloud", + "type": "Formula", + "effort": "HIGH", + "variants": [ + "kafka-to-eventhubs" + ], + "tooltip": "Migrate from Kafka to Apache Kafka on Confluent Cloud with managed identity for secure, credential-free authentication." + }, + { + "solutionId": "kafka-to-eventhubs", + "name": "Migrate from Kafka to Azure Event Hubs for Apache Kafka", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Kafka to Azure Event Hubs for Apache Kafka with managed identity for secure, credential-free authentication." + }, + { + "solutionId": "other-cache-solutions-to-azure-managed-cache", + "name": "Migrate Other Cache Solutions to Azure Managed Redis", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from other cache solutions (like Apache Commons JCS, Ehcache, Hazelcast, Infinispan, or local Redis/session) to Azure Managed Redis." + }, + { + "solutionId": "log-to-console", + "name": "Migrate to Console Logging", + "type": "Formula", + "effort": "MEDIUM", + "tooltip": "Migrate from file-based logging to console logging to support cloud-native apps and integration with Azure Monitor." + }, + { + "solutionId": "mi-azure-sql", + "name": "Secure Azure SQL Database with Managed Identity", + "type": "Formula", + "effort": "MEDIUM", + "tooltip": "Secure Azure SQL Database with Managed Identity." + }, + { + "solutionId": "mi-cassandra", + "name": "Secure Azure Cosmos DB for Cassandra with Service Connector", + "type": "Formula", + "effort": "LOW", + "tooltip": "Secure Azure Cosmos DB for Cassandra with Service Connector for a fully managed, scalable database with Cassandra API support." + }, + { + "solutionId": "mi-mariadb", + "name": "Migrate to Azure Database for MariaDB (Spring)", + "type": "Formula", + "effort": "MEDIUM", + "tooltip": "Migrate from MariaDB to Azure Database for MariaDB with managed identity for secure, credential-free authentication." + }, + { + "solutionId": "mi-mongodb", + "name": "Secure Azure DocumentDB (with MongoDB Compatibility) with Microsoft Entra ID Authentication", + "type": "Formula", + "effort": "LOW", + "tooltip": "Secure Azure DocumentDB (with MongoDB Compatibility) with Microsoft Entra ID authentication." + }, + { + "solutionId": "mi-mysql", + "name": "Migrate to Azure Database for MySQL", + "type": "Formula", + "effort": "MEDIUM", + "tooltip": "Migrate from MySQL to Azure Database for MySQL with managed identity for secure, credential-free authentication." + }, + { + "solutionId": "mi-postgresql", + "name": "Secure Azure Database for PostgreSQL with Managed Identity", + "type": "Formula", + "effort": "MEDIUM", + "tooltip": "Secure Azure Database for PostgreSQL with Managed Identity." + }, + { + "solutionId": "AWS-secrets-manager-to-azure-key-vault", + "name": "Migrate from AWS Secrets Manager to Azure Key Vault", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from AWS Secrets Manager to Azure Key Vault to securely manage and access sensitive information in Azure." + }, + { + "solutionId": "certificate-management-to-azure-key-vault", + "name": "Migrate from KeyStore to Azure Key Vault", + "type": "Formula", + "effort": "LOW", + "tooltip": "Migrate from a local KeyStore to Azure Key Vault for secure storage and access to certificates and keys." + }, + { + "solutionId": "local-files-to-mounted-azure-storage", + "name": "Migrate to Azure Storage Account File Share mounts", + "type": "Formula", + "effort": "LOW", + "tooltip": "Migrate from local file system to Azure Storage Account File Share mounts for scalable and secure file storage." + }, + { + "solutionId": "on-premises-user-authentication-to-microsoft-entra-id", + "name": "Migrate from on-premises user authentication to Microsoft Entra ID", + "description": "TODO: need to further check if this aligns with the solution", + "type": "Formula", + "effort": "MEDIUM", + "tooltip": "Migrate from on-premises user authentication systems to Microsoft Entra ID for secure and scalable user management in Azure." + }, + { + "solutionId": "plaintext-credential-to-azure-keyvault", + "name": "Migrate from Plaintext Credentials to Azure Key Vault", + "type": "Formula", + "effort": "LOW", + "tooltip": "Migrate from plaintext credentials in the code to Azure Key Vault for storage and access to sensitive information." + }, + { + "solutionId": "s3-to-azure-blob-storage", + "name": "Migrate from AWS S3 to Azure Blob Storage", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from AWS S3 to Azure Blob Storage for scalable and secure object storage in Azure." + }, + { + "solutionId": "spring-jms-rabbitmq-servicebus", + "name": "Migrate from RabbitMQ(JMS) to Azure Service Bus", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from RabbitMQ with JMS to Azure Service Bus for a managed messaging service with JMS API support." + }, + { + "solutionId": "sqs-to-servicebus", + "name": "Migrate from AWS Simple Queue Service to Azure Service Bus", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from AWS Simple Queue Service to Azure Service Bus for a managed messaging service with advanced features." + }, + { + "solutionId": "oracle-to-postgresql", + "name": "Migrate from Oracle DB to PostgreSQL", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Oracle DB to PostgreSQL" + }, + { + "solutionId": "bare/redesign-java-gui-app", + "name": "Redesign Java GUI application to migrate it to Azure", + "type": "Chat", + "prompt": "The application uses desktop GUI framework which requires desktop runtime and is not cloud-native, consider containerizing for Azure deployment or redesigning as a web application.", + "tooltip": "Redesign the Java app's graphical user interface (GUI) using Java Swing or JavaFX to migrate it to Azure." + }, + { + "solutionId": "bare/apm-to-application-insights", + "name": "Migrate APM to application insights", + "type": "Chat", + "prompt": "The app uses an application performance monitoring (APM) platform. To migrating Java app to Azure, use Azure Monitor or Application Insights for built-in tracing and auto-instrumentation support in Azure services.", + "tooltip": "The app uses an application performance monitoring (APM) platform. Chat with Copilot to learn how to migrate APM to Azure Monitor or Application Insights." + }, + { + "solutionId": "bare/encoding-standards", + "name": "Check Encoding in the Code", + "type": "Chat", + "prompt": "The code uses UTF-8 by default. If it's not appropriate for your code, use a different character set.", + "tooltip": "The code uses UTF-8 by default. Chat with Copilot to learn how to check and update the encoding." + }, + { + "solutionId": "bare/local-resource-access", + "name": "Migrate the Local Resource to Azure", + "type": "Chat", + "prompt": "The application is using some resource or service from localhost. When you migrate the application to Azure, you also need to migrate the dependent resource or service to Azure.", + "tooltip": "The app uses some resource or service from localhost. Chat with Copilot to learn how to migrate the local resource to Azure." + }, + { + "solutionId": "bare/remote-communication", + "name": "Use Loosely coupled protocols in Cloud Environment", + "type": "Chat", + "prompt": "The app uses legacy protocols. Please use loosely coupled protocols like REST, gRPC, etc.", + "tooltip": "The app uses legacy protocols. Chat with Copilot to learn how to use loosely coupled protocols like REST, gRPC, etc." + }, + { + "solutionId": "bare/remote-communication/java-socket", + "name": "Use Java Socket Communication in Cloud Environment", + "type": "Chat", + "prompt": "The application uses Java socket communication, which depends on fixed IP addresses and ports, making it unsuitable for cloud environments where service endpoints are dynamic and scaling is required. Replace socket-based communication with cloud-friendly, loosely coupled alternatives such as RESTful APIs, gRPC, JMS messaging, Azure Service Bus, etc.", + "tooltip": "The app uses legacy protocols. Chat with Copilot to learn how to use loosely coupled protocols like REST, gRPC, etc." + }, + { + "solutionId": "bare/remote-communication/corba", + "name": "Check CORBA usage", + "type": "Chat", + "prompt": "The application uses CORBA which is tightly coupled and not suitable for cloud environments. Replace with REST APIs, gRPC, or Azure Service Bus for messaging. Use Azure API Management for API gateway capabilities.", + "tooltip": "The app uses CORBA for remote communication. Chat with Copilot to learn how to review and update it when migrating to Azure." + }, + { + "solutionId": "bare/remote-communication/hardcode-ip", + "name": "Check hardcoded IP address", + "type": "Chat", + "prompt": "The application uses hardcoded IP addresses. When migrating to Azure cloud, review and update any hardcoded IP addresses as needed, or migrate the dependent services accordingly.", + "tooltip": "The app uses hardcoded IP addresses. Chat with Copilot to learn how to review and update them when migrating to Azure." + }, + { + "solutionId": "bare/remote-communication/secure-protocols", + "name": "Use Secure Protocols", + "type": "Chat", + "prompt": "The application uses insecure protocols. When migrating to Azure cloud, review and update any insecure protocols to secure protocols such as HTTPS and SFTP (over HTTP and FTP).", + "tooltip": "The app uses insecure protocols. Chat with Copilot to learn how to switch to secure ones like HTTPS or SFTP." + }, + { + "solutionId": "bare/remote-communication/hardcoded-urls", + "name": "Check hardcoded URLs", + "type": "Chat", + "prompt": "The application uses hardcoded URLs. When migrating to Azure cloud, review and update any hardcoded URLs as needed, or migrate the dependent services accordingly.", + "tooltip": "The app uses hardcoded URLs. Chat with Copilot to learn how to review and update them when migrating to Azure." + }, + { + "solutionId": "bare/os-compatibility", + "name": "Redesign OS Specific Code", + "type": "Chat", + "prompt": "The app uses a Windows Dynamic-Link Library (DLL). Redesign the code to avoid using OS specific code.", + "tooltip": "The app uses a Windows Dynamic-Link Library (DLL). Chat with Copilot to learn how to redesign the code to avoid using OS specific code." + }, + { + "solutionId": "bare/java-native-code", + "name": "Build Native Process into Container Image", + "type": "Chat", + "prompt": "The application uses Java native libraries (JNI, JNA) which may not be compatible with cloud container environments. Identify these dependencies and either containerize them with matching base images or replace them with platform-independent libraries, cloud-native solutions, or Azure managed services.", + "tooltip": "The app uses Java native libraries (JNI, JNA). Chat with Copilot to learn how to build them into containers or find Azure alternatives." + }, + { + "solutionId": "bare/jakataee-to-azure", + "name": "Deploy JakartaEE App to Azure", + "type": "Chat", + "prompt": "The Application relies on Jakarta EE APIs. Azure provides support for Jakarta EE applications from different vendors, including Red Hat OpenShift, IBM WebSphere Liberty, and Oracle WebLogic Server.", + "tooltip": "The app uses Jakarta EE APIs. Chat with Copilot to learn how to deploy them to Azure using supported vendors." + }, + { + "solutionId": "bare/jakataee-to-azure/rmi", + "name": "Check Java Remote Method Invocation(RMI)", + "type": "Chat", + "prompt": "The application uses Java RMI which is tightly coupled and not cloud-ready, replace it with HTTP-based RESTful APIs for standard communication or Azure Service Bus for messaging scenarios.", + "tooltip": "The app uses Java RMI. Chat with Copilot to learn how to replace it with cloud-ready alternatives." + }, + { + "solutionId": "bare/jakataee-to-azure/jca", + "name": "Check Java Connector Architecture(JCA)", + "type": "Chat", + "prompt": "The application uses Java Connector Architecture(JCA) which is tightly coupled and not suitable for cloud scalability, replace with appropriate Azure managed services like Azure Service Bus, Azure Event Hub, etc.", + "tooltip": "The app uses Java Connector Architecture(JCA). Chat with Copilot to learn how to replace it with cloud-ready alternatives." + }, + { + "solutionId": "bare/configuration-management/environment-variables", + "name": "Configure System Environment Variables", + "type": "Chat", + "prompt": "The application uses environment variables or system properties. When migrating to Azure, they need to be passed according to the target hosting service's setup. If they contain sensitive information, it's better to store in KeyVault. If some configurations are shared, Azure App Configuration service may be an option to store them.", + "tooltip": "The app uses environment variables or system properties. Chat with Copilot to learn how to configure system environment variables when migrating to Azure." + }, + { + "solutionId": "bare/configuration-management/external-configuration", + "name": "Manage External Configuration", + "type": "Chat", + "prompt": "The app stores settings in external files other than web.config. When migrating to Azure, they need to be passed according to the target hosting services's setup. If they contain sensitive information, it's better to store in KeyVault. If some configurations are shared, Azure App Configuration service may be an option to store them.", + "tooltip": "The app stores settings in external files. Chat with Copilot to learn how to manage external configuration when migrating to Azure." + }, + { + "solutionId": "bare/configuration-management/windows-registry", + "name": "Manage Windows Registry Configuration", + "type": "Chat", + "prompt": "The application writes application settings into OS-specific storage such as Windows Registry. When migrating to Azure, these application settings should not be defined in such storage. If they contain sensitive information, it's better to store in KeyVault. If some configurations are shared, Azure App Configuration service may be an option to store them.", + "tooltip": "The app stores settings in OS-specific storage like the Windows Registry. Chat with Copilot to learn managing Windows Registry configuration when migrating to Azure." + }, + { + "solutionId": "quartz-scheduler-to-azure-functions", + "name": "Migrate from Quartz Scheduler to Azure Functions", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Quartz Scheduler to Azure Functions for serverless, event-driven task scheduling in Azure.", + "experimental": true + }, + { + "solutionId": "bare/spring-migration", + "name": "Deploy Spring Cloud App To Azure", + "type": "Chat", + "prompt": "The application has Spring Boot or Spring Cloud dependencies. Azure Container Apps offers managed components for Spring Cloud, so it may be an option for Spring Cloud migration. Special attention is required for some environment related settings, such as server.port, Config Server or Eureka bindings.", + "tooltip": "The app uses Spring Boot or Spring Cloud. Chat with Copilot to learn how to deploy Spring Cloud apps to Azure using Azure Container Apps." + }, + { + "solutionId": "bare/eap-migration/jboss-eap", + "name": "Deploy JBoss EAP to Azure", + "type": "Chat", + "prompt": "The app uses JBoss EAP related code, configs, dependencies, and/or environment settings. JBoss EAP is a Java EE application server available on Azure.", + "tooltip": "The app uses JBoss EAP related code, configs, dependencies, and/or environment settings. Chat with Copilot to learn how to deploy JBoss EAP apps to Azure using supported vendors." + }, + { + "solutionId": "bare/azure-service-connector", + "name": "Use Azure Service Connector", + "type": "Chat", + "prompt": "The app uses VMware Tanzu Application Service (TAS) service bindings. In Azure, use Azure Service Connect to link to Azure services.", + "tooltip": "The app uses VMware Tanzu Application Service (TAS) service bindings. Chat with Copilot to learn how to use Azure Service Connector to connect to Azure services." + }, + { + "solutionId": "bare/aws-region-configuration-to-azure", + "name": "Migrate from AWS Region Configuration to Azure Region Configuration", + "type": "Chat", + "prompt": "The app has AWS region settings. Identify the AWS service to migrate, find Azure alternatives, and check their region availability. Provide the links to Azure docs page for latest region availability.", + "tooltip": "The app has AWS region settings. Chat with Copilot to learn how to migrate from AWS region configuration to Azure region configuration." + }, + { + "solutionId": "bare/spring-cloud-vault-migration", + "name": "Migrate from Spring Cloud Vault to Azure Key Vault", + "type": "Chat", + "prompt": "The application integrates with Spring Cloud Vault. To migrate a Java application that uses Spring Cloud Vault to Azure, you should identify all secrets and the backing secret store, then migrate them to Azure Key Vault. Use the Azure Key Vault Spring Boot Starter for secret injection. You may need to rename some secrets and update references in the application code.", + "tooltip": "The app integrates with Spring Cloud Vault. Chat with Copilot to learn how to migrate from Spring Cloud Vault to Azure Key Vault." + }, + { + "solutionId": "bare/aws-credentials-to-azure", + "name": "Migrate from AWS Access Key ID/Secret to Azure Credentials", + "type": "Chat", + "prompt": "The application contains AWS credential configuration. We need to find out what AWS service we want to migrate, find the candidate alternatives on Azure, do the code changes according to the source service and target service. Secrets should be stored in Azure Key Vault. The best practice is to use DefaultAzureCredential to authenticate to Azure and access the target service.", + "tooltip": "The app has AWS credential configuration. Chat with Copilot to learn how to migrate from AWS access key ID/secret to Azure credentials." + }, + { + "solutionId": "bare/openliberty-migration/openliberty-database", + "name": "Migrate from Open Liberty Database to Azure Database Services", + "type": "Chat", + "prompt": "The application uses Open Liberty database configurations and datasources. When migrating to Azure, identify the specific database type (MySQL, PostgreSQL, SQL Server) and migrate to the appropriate Azure Database service. Use connection pooling optimized for cloud environments and configure Azure Key Vault to securely store connection strings. Implement DefaultAzureCredential for managed identity authentication to eliminate hard-coded credentials. Consider using Azure App Configuration for centralized connection management across environments.", + "tooltip": "The app uses Open Liberty database configurations and datasources. Chat with Copilot to learn how to migrate from Open Liberty database to Azure Database Services." + }, + { + "solutionId": "bare/openliberty-migration/openliberty-filesystem", + "name": "Migrate from Open Liberty Filesystem to Azure Storage", + "type": "Chat", + "prompt": "The application uses Open Liberty filesystem for data storage or configuration. When migrating to Azure, replace local filesystem dependencies with Azure Blob Storage or Azure Files depending on access patterns. For read-heavy shared configuration, consider Azure Blob Storage with CDN. For applications requiring file system mounting, use Azure Files with SMB protocol. Implement the Azure Storage SDK with DefaultAzureCredential for secure access, and store any access keys in Azure Key Vault. Consider data access patterns when selecting storage tier and replication options.", + "tooltip": "The app uses Open Liberty filesystem for data storage or configuration. Chat with Copilot to learn how to migrate from Open Liberty filesystem to Azure Storage." + }, + { + "solutionId": "bare/openliberty-migration/openliberty-jms", + "name": "Migrate from Open Liberty JMS to Azure Service Bus", + "type": "Chat", + "prompt": "The application uses Open Liberty Java Message Service (JMS) for messaging. When migrating to Azure, Azure Service Bus is the recommended alternative. Analyze current JMS usage patterns (queues, topics, message selectors) to map to Service Bus concepts. Use the JMS over AMQP provider with the Azure Service Bus SDK for Java. Implement DefaultAzureCredential for authentication, store connection strings in Azure Key Vault, and adjust client-side configurations for cloud reliability patterns like retry policies and circuit breakers.", + "tooltip": "The app uses Open Liberty Java Message Service (JMS) for messaging. Chat with Copilot to learn how to migrate from Open Liberty JMS to Azure Service Bus." + }, + { + "solutionId": "bare/openliberty-migration/openliberty-logging", + "name": "Migrate from Open Liberty Logging to Azure Monitor", + "type": "Chat", + "prompt": "The application uses Open Liberty logging configurations. When migrating to Azure, implement a cloud-native logging strategy using Azure Monitor and Application Insights. Configure the Application Insights Java agent for auto-instrumentation or use the Application Insights SDK for more customization. For structured logging, consider Log Analytics workspace integration. Update logging configurations to use console output (stdout/stderr) instead of files when deployed to Azure App Service or Azure Container Apps. Implement correlation IDs across services for distributed tracing and use Azure Monitor Workbooks for custom dashboards.", + "tooltip": "The app uses Open Liberty logging configurations. Chat with Copilot to learn how to migrate from Open Liberty logging to Azure Monitor." + }, + { + "solutionId": "bare/oraclejdk-to-openjdk/resource-management-apis", + "name": "Update Resource Management APIs for migration from Oracle JDK to OpenJDK", + "type": "Chat", + "prompt": "The application uses Resource Management APIs. When migrating to OpenJDK, OpenJDK does not support the resource management API for Java, review and update the resource management API usage to ensure compatibility with OpenJDK. Specifically, identify and replace the use of classes and methods from the `jdk.management.resource` package with alternative approaches for resource monitoring and management.", + "tooltip": "The app uses Resource Management APIs. Chat with Copilot to learn how to update Resource Management APIs for migration from Oracle JDK to OpenJDK." + }, + { + "solutionId": "bare/oraclejdk-to-openjdk/imageio", + "name": "Replace ImageIO usage for migration from Oracle JDK to OpenJDK", + "type": "Chat", + "prompt": "The application uses Oracle JDK JPEG image encoder/decoder usage. When migrating to OpenJDK, review and update the image encoder/decoder usage to ensure compatibility with OpenJDK. Specifically, identify and replace the use of classes and methods from the `com.sun.image.codec.jpeg` package with `javax.imageio.ImageIO`.", + "tooltip": "The application uses Oracle JDK JPEG image encoder/decoder usage. Chat with Copilot to learn how to replace ImageIO usage for migration from Oracle JDK to OpenJDK." + }, + { + "solutionId": "bare/database-migration/database-reliability", + "name": "Update database configurations for cloud readiness and resilience", + "type": "Chat", + "prompt": "The application uses database. When migrating to Azure, review and update the database configurations to ensure readiness for Azure cloud deployment. Specifically, identify any on-premise specific settings that are incompatible or suboptimal for Azure; recommend updates to support high availability, automatic failover, and geo-redundancy; ensure connection strings support retry policies, transient fault handling, and use managed identity authentication if possible; detect hardcoded paths, IPs, or dependencies that may need reconfiguration.", + "tooltip": "The app uses database. Chat with Copilot to learn how to update database configurations for cloud readiness and resilience." + }, + { + "solutionId": "bare/jakarta-auth-migration", + "name": "Migrate Jakarta EE Authentication to Microsoft Entra ID", + "type": "Chat", + "prompt": "The application uses Jakarta Authentication and Authorization APIs. When migrating to Azure, how should I modernize the authentication to integrate with Microsoft Entra ID? Please provide: 1. Code examples for replacing Jakarta Authentication with OAuth 2.0/OIDC. 2. Microsoft Entra ID configuration steps (App Registration, permissions) 3. Best practices for container-based authentication on Azure. 4. Authorization strategy (RBAC vs application claims). Include configuration samples and highlight key migration considerations.", + "tooltip": "The app uses Jakarta Authentication and Authorization APIs. Chat with Copilot to learn how to migrate Jakarta EE Authentication to Microsoft Entra ID." + }, + { + "solutionId": "bare/jakarta-websocket-migration", + "name": "Migrate Jakarta EE WebSocket", + "type": "Chat", + "prompt": "The application uses Jakarta WebSocket APIs. When migrating to Azure, please advise on: 1. Best Azure service for hosting WebSocket applications (self-hosted vs Azure Web PubSub)? 2. Code examples for migrating @ServerEndpoint to Azure-compatible patterns. 3. Required Azure configurations: session affinity, TLS, connection timeouts. 4. How to integrate Microsoft Entra ID authentication for WebSocket connections? 5. Load balancing and scalability considerations for real-time connections. Include code samples, configuration examples, and migration trade-offs.", + "tooltip": "The app uses Jakarta WebSocket APIs. Chat with Copilot to learn how to migrate Jakarta EE WebSocket." + }, + { + "solutionId": "bare/jakarta-jaxrs-migration", + "name": "Migrate Jakarta JAX-RS to Azure", + "type": "Chat", + "prompt": "My Java application uses Jakarta JAX-RS APIs (jakarta.ws.rs.* or javax.ws.rs.*) on a Jakarta EE/MicroProfile runtime. I need to migrate to Azure. Please advise on: 1. Deployment options - Azure App Service, AKS, or Container Apps for JAX-RS applications? 2. Configuration externalization - migrating to Azure App Configuration and Key Vault with code examples. 3. API security - securing JAX-RS endpoints with Microsoft Entra ID, OAuth 2.0/OIDC filters, and JWT validation in JAX-RS filters and interceptors. 4. Observability - integrating Azure Application Insights for telemetry and distributed tracing. 5. Production readiness - HTTPS configuration, Managed Identity, auto-scaling, and health checks. Include code examples, Azure configuration samples, and migration checklist.", + "tooltip": "The application uses Jakarta JAX-RS APIs for RESTful services. Chat with Copilot to learn how to migrate to Azure App Service, AKS, or Container Apps with proper security and monitoring." + }, + { + "solutionId": "bare/jakarta-nosql-migration", + "name": "Migrate Jakarta NoSQL to Azure", + "type": "Chat", + "prompt": "My application uses Jakarta NoSQL APIs (jakarta.nosql.*). I need to migrate to Azure. Please advise on: 1. Should I migrate to Azure Cosmos DB native SDKs? Which Cosmos DB API (NoSQL, MongoDB, Cassandra, Gremlin, Table) matches my data model? 2. How to update data access layer from Jakarta NoSQL to Cosmos DB SDK with code examples? 3. Configuration - connection strings, authentication (Managed Identity), and security. 4. Network security - VNet integration, private endpoints, and firewall rules. 5. Performance - throughput settings, consistency levels, and optimization. Include before/after code examples and Azure configuration.", + "tooltip": "The application uses Jakarta NoSQL APIs. Chat with Copilot to learn how to migrate to Azure Cosmos DB." + }, + { + "solutionId": "bare/jakarta-persistence-migration", + "name": "Migrate Jakarta JPA to Azure", + "type": "Chat", + "prompt": "My application uses Jakarta JPA APIs (jakarta.persistence.* or javax.persistence.*) with Hibernate/EclipseLink. I need to migrate to Azure. Please advise on: 1. Which Azure database - PostgreSQL, MySQL, or SQL Database? 2. Updating persistence.xml/properties for Azure connections, dialect, and connection pools. 3. Storing credentials in Azure Key Vault with Managed Identity examples. 4. Network security - VNet integration, private endpoints, and firewall rules. 5. Deployment on Azure App Service, AKS, or Container Apps. Include configuration examples and Spring Data JPA guidance.", + "tooltip": "The application uses Jakarta JPA APIs. Chat with Copilot to learn how to migrate to Azure database services." + }, + { + "solutionId": "bare/jakarta-data-migration", + "name": "Migrate Jakarta Data to Azure", + "type": "Chat", + "prompt": "My application uses Jakarta Data APIs (jakarta.data.*) for repository-based data access. I need to migrate to Azure. Please advise on: 1. For relational workloads: Azure PostgreSQL/MySQL/SQL Database; for NoSQL: Azure Cosmos DB - which fits my use case? 2. Ensuring Jakarta Data providers (Eclipse JNoSQL, Micronaut Data) work with Azure. 3. Updating repository configuration for Azure with connection URLs and credentials. 4. Network security - VNet integration, private endpoints, and firewall rules. 5. Using Azure Key Vault for credential management. 6. Deployment on Azure App Service, AKS, or Container Apps. Include configuration examples.", + "tooltip": "The application uses Jakarta Data APIs. Chat with Copilot to learn how to migrate to Azure databases." + }, + { + "solutionId": "bare/jboss-eap-to-azure-app-service", + "name": "Migrate JBoss EAP to Azure App Service", + "type": "Chat", + "prompt": "My application uses JBoss EAP and I need to migrate to Azure App Service. Please advise on: 1. Preparing JBoss EAP application for Azure App Service deployment. 2. Configuring JBoss EAP runtime (version, startup settings) and updating build files. 3. Managing configuration with Azure App Configuration and Key Vault. 4. Deployment options - Maven/Gradle plugins or CI/CD. 5. Setting up monitoring with Application Insights. Include configuration examples.", + "tooltip": "The application uses JBoss EAP. Chat with Copilot to learn how to migrate to JBoss EAP on Azure App Service." + }, + { + "solutionId": "bare/jboss-eap-to-aks", + "name": "Migrate JBoss EAP to Azure Kubernetes Service", + "type": "Chat", + "prompt": "My application uses JBoss EAP and I need to migrate to AKS. Please advise on two options: Option 1 - Lift-and-Shift: Using Red Hat JBoss EAP container images, Dockerfile examples, and Kubernetes manifests (Deployment, Service, ConfigMap). Option 2 - Refactor: Migrating to Spring Boot/Quarkus/Micronaut with code refactoring patterns. For both: include networking, scaling, Azure Key Vault integration, and monitoring. Help me choose the right approach with code examples.", + "tooltip": "The application uses JBoss EAP. Chat with Copilot to learn migration to AKS: lift-and-shift or refactor to cloud-native." + }, + { + "solutionId": "bare/jboss-eap-to-azure-container-apps", + "name": "Migrate JBoss EAP to Azure Container Apps", + "type": "Chat", + "prompt": "My application uses JBoss EAP and I need to migrate to Azure Container Apps. Please advise on two options: Option 1 - Lift-and-Shift: Using Red Hat JBoss EAP container images and Dockerfile examples. Option 2 - Refactor: Migrating to Spring Boot/Quarkus/Micronaut with refactoring patterns. For both: include Container Apps configuration (scaling, traffic splitting), Azure Key Vault, ingress, and Application Insights. Help me choose with code examples.", + "tooltip": "The application uses JBoss EAP. Chat with Copilot to learn migration to Container Apps: lift-and-shift or refactor." + }, + { + "solutionId": "bare/weblogic-to-azure-app-service", + "name": "Migrate WebLogic to JBoss EAP on Azure App Service", + "type": "Chat", + "prompt": "My application uses WebLogic Server and I need to migrate to JBoss EAP on Azure App Service. Please advise on: 1. Key differences between WebLogic and JBoss EAP. 2. Migrating weblogic.xml and descriptors to JBoss equivalents. 3. Updating build files to replace WebLogic dependencies. 4. Configuring JBoss EAP runtime on App Service. 5. Data sources, JNDI, and JMS setup. Include configuration examples and checklist.", + "tooltip": "The application uses WebLogic Server. Chat with Copilot to learn how to migrate to JBoss EAP on Azure App Service." + }, + { + "solutionId": "bare/weblogic-to-aks", + "name": "Migrate WebLogic to Azure Kubernetes Service", + "type": "Chat", + "prompt": "My application uses WebLogic Server and I need to migrate to AKS. Please advise on: 1. Migration approach - WebLogic on AKS or refactor to cloud-native? 2. Using Oracle WebLogic Kubernetes Operator. 3. Containerizing WebLogic with Dockerfile examples. 4. Kubernetes manifests for WebLogic domains and clusters. 5. Networking, secrets with Azure Key Vault, and monitoring. Include code examples and architecture guidance.", + "tooltip": "The application uses WebLogic Server. Chat with Copilot to learn how to migrate to Azure Kubernetes Service." + }, + { + "solutionId": "bare/weblogic-to-azure-container-apps", + "name": "Migrate WebLogic to Azure Container Apps", + "type": "Chat", + "prompt": "My application uses WebLogic Server and I need to migrate to Azure Container Apps. Please advise on two options: Option 1 - Lift-and-Shift: WebLogic container images and Dockerfile examples. Option 2 - Refactor: Migrating to Spring Boot/Quarkus/Micronaut. For both: include Container Apps configuration, Azure Key Vault, autoscaling, and monitoring. Help me choose with code examples.", + "tooltip": "The application uses WebLogic Server. Chat with Copilot to learn migration to Container Apps: lift-and-shift or refactor." + }, + { + "solutionId": "bare/websphere-to-azure-app-service", + "name": "Migrate WebSphere to JBoss EAP on Azure App Service", + "type": "Chat", + "prompt": "My application uses WebSphere and I need to migrate to JBoss EAP on Azure App Service. Please advise on: 1. Key differences between WebSphere and JBoss EAP. 2. Migrating WebSphere descriptors (ibm-web-ext.xml, ibm-application-bnd.xml) to JBoss equivalents. 3. Replacing WebSphere dependencies (com.ibm.websphere.*) in build files. 4. Configuring JBoss EAP runtime on App Service. 5. Data sources, JNDI, and messaging setup. Include configuration examples and checklist.", + "tooltip": "The application uses WebSphere. Chat with Copilot to learn how to migrate to JBoss EAP on Azure App Service." + }, + { + "solutionId": "bare/websphere-to-aks", + "name": "Migrate WebSphere to Azure Kubernetes Service", + "type": "Chat", + "prompt": "My application uses WebSphere and I need to migrate to AKS. Please advise on: 1. Migration approach - WebSphere Liberty on AKS or refactor to cloud-native? 2. Using IBM WebSphere Liberty Operator for Kubernetes. 3. Containerizing WebSphere applications with Dockerfile examples. 4. Kubernetes manifests for WebSphere Liberty deployment. 5. Networking, secrets with Azure Key Vault, and monitoring. Include code examples and architecture guidance.", + "tooltip": "The application uses WebSphere. Chat with Copilot to learn how to migrate to Azure Kubernetes Service." + }, + { + "solutionId": "bare/websphere-to-azure-container-apps", + "name": "Migrate WebSphere to Azure Container Apps", + "type": "Chat", + "prompt": "My application uses WebSphere and I need to migrate to Azure Container Apps. Please advise on two options: Option 1 - Lift-and-Shift: IBM WebSphere Liberty container images and Dockerfile examples. Option 2 - Refactor: Migrating to Spring Boot/Quarkus/Micronaut. For both: include Container Apps configuration, Azure Key Vault, autoscaling, and monitoring. Help me choose with code examples.", + "tooltip": "The application uses WebSphere. Chat with Copilot to learn migration to Container Apps: lift-and-shift or refactor." + }, + { + "solutionId": "bare/appserver-api-migration-to-standard-java", + "name": "Migrate Proprietary App Server APIs to Standard Java/Jakarta EE", + "type": "Chat", + "prompt": "The application uses proprietary application server APIs (WebLogic, WebSphere, JBoss EAP, or JBoss Seam) that must be migrated to standard Java/Jakarta EE equivalents. This is an application server portability migration — not a simple JDK deprecated API fix — and typically involves significant code changes across imports, annotations, deployment descriptors, and build dependencies.\n\nPlease analyze the detected issues and provide file-level migration guidance based on these patterns:\n\n1. **CommonJ Timer/Work Manager (WebLogic or WebSphere)**: Replace `commonj.timers.*` with `java.util.concurrent.ScheduledExecutorService`; replace `commonj.work.*` with `java.util.concurrent.ExecutorService` or Jakarta Concurrency `ManagedExecutorService` (`jakarta.enterprise.concurrent`).\n\n2. **Vendor-specific JMS (WebLogic/WebSphere JMS)**: Replace `weblogic.jms.*` or `com.ibm.websphere.jms.*` with standard Jakarta JMS (`jakarta.jms.*`). Update connection factory lookups to use standard JNDI; remove vendor-specific extensions for destinations, connection pooling, and message handling.\n\n3. **WebLogic Servlet/Lifecycle**: Replace `weblogic.application.ApplicationLifecycleListener` with standard `jakarta.servlet.ServletContextListener` or `@WebListener`. Replace WebLogic-specific servlet classes with standard Servlet API equivalents.\n\n4. **WebLogic WebServices**: Migrate from `weblogic.wsee.*` proprietary annotations and descriptors to standard JAX-WS (`jakarta.xml.ws.*`) or JAX-RS (`jakarta.ws.rs.*`). Remove WebLogic-specific web service deployment descriptors.\n\n5. **WebLogic Webapp Descriptors**: Replace `weblogic.xml` and vendor-specific deployment descriptors with standard `web.xml` or annotation-based configuration.\n\n6. **JBoss EAP Cross-Version Migration**: Replace deprecated JBoss-internal classes (logging, transactions, classloading) with standard Java/Jakarta EE equivalents or updated JBoss APIs.\n\n7. **JBoss Seam → CDI**: Replace Seam annotations (`@Name`, `@In`, `@Out`, `@Factory`) with CDI equivalents (`@Named`, `@Inject`, `@Produces`). Refactor Seam interceptors, page flows, and bijection to CDI interceptors, decorators, and standard scopes.\n\n8. **CDI Deprecated API**: Update deprecated CDI methods (e.g., `Bean#isNullable()`, `BeanManager.fireEvent()`) to current Jakarta CDI replacements.\n\n9. **JBoss Deprecated Dependencies**: Replace deprecated JBoss-specific dependencies with their standard Java/Jakarta EE or community-maintained equivalents.\n\nGeneral approach: (a) Scan for vendor-specific package imports to build an inventory. (b) Map each proprietary class to its standard equivalent. (c) Refactor incrementally per module — update imports, class references, method signatures and descriptors. (d) Remove vendor SDK dependencies from pom.xml/build.gradle and add standard Jakarta EE API dependencies. (e) Validate with integration tests, especially messaging, lifecycle hooks, and web service endpoints.", + "tooltip": "The app uses proprietary app server APIs (WebLogic, WebSphere, JBoss). Chat with Copilot to learn how to migrate to standard Java/Jakarta EE equivalents." + }, + { + "solutionId": "eclipse-project-to-maven-project", + "name": "Migrate from Eclipse Project to Maven Project", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate current project from eclipse project to maven project" + }, + { + "solutionId": "ant-project-to-maven-project", + "name": "Migrate from Ant Project to Maven Project", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate current project from Ant project to Maven project" + }, + { + "solutionId": "containerization-copilot-agent", + "name": "Containerize Java Application for Container Readiness", + "type": "Formula", + "effort": "HIGH", + "tooltip": "The app does not have a Dockerfile and/or is not container-ready. Use Agent Mode with Copilot to create and execute a containerization plan." + }, + { + "solutionId": "google-cloud-pub-sub-to-azure-service-bus", + "name": "Migrate from Google Pub/Sub to Azure Service Bus", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Google Pub/Sub to Azure Service Bus for reliable and secure messaging in Azure.", + "experimental": true + }, + { + "solutionId": "google-gcr-to-azure-acr", + "name": "Migrate from Google GCR to Azure ACR", + "type": "Chat", + "prompt": "The application uses Google Container Registry (GCR) for container image storage. To migrate to Azure, use Azure Container Registry (ACR) as the alternative container registry service. Set up an ACR instance, configure authentication using Azure Active Directory and DefaultAzureCredential, and update deployment pipelines to push/pull images from ACR. Consider using Azure Container Apps or Azure Kubernetes Service (AKS) for hosting containerized applications.", + "tooltip": "Migrate from Google GCR to Azure ACR for reliable and secure container registry in Azure." + }, + { + "solutionId": "spring-cloud-config-to-azure-app-configuration", + "name": "Migrate from Spring Cloud Config to Azure App Configuration", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Spring Cloud Config to Azure App Configuration for scalable and secure configuration management in Azure.", + "experimental": true + }, + { + "solutionId": "sybase-ase-to-azure-postgresql", + "name": "Migrate from Sybase ASE to Azure PostgreSQL", + "type": "Formula", + "effort": "HIGH", + "variants": [ + "sybase-ase-to-azure-sql-database" + ], + "tooltip": "Migrate from Sybase ASE to Azure PostgreSQL for scalable and secure database management in Azure.", + "experimental": true + }, + { + "solutionId": "sybase-ase-to-azure-sql-database", + "name": "Migrate from Sybase ASE to Azure SQL Database", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Sybase ASE to Azure SQL Database for scalable and secure database management in Azure." + }, + { + "solutionId": "google-firestore-to-azure-cosmos-db", + "name": "Migrate from Google Firestore to Azure Cosmos DB", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Google Firestore to Azure Cosmos DB for scalable and secure NoSQL database management in Azure.", + "experimental": true + }, + { + "solutionId": "google-cloud-bigtable-to-azure-cosmos-db", + "name": "Migrate from Google Cloud Bigtable to Azure Cosmos DB", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Google Cloud Bigtable to Azure Cosmos DB for scalable and secure NoSQL database management in Azure.", + "experimental": true + }, + { + "solutionId": "google-cloud-spanner-to-azure-postgresql", + "name": "Migrate from Google Cloud Spanner to Azure PostgreSQL", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Google Cloud Spanner to Azure PostgreSQL for scalable and secure database management in Azure.", + "experimental": true + }, + { + "solutionId": "apache-pulsar-to-azure-event-hubs", + "name": "Migrate from Apache Pulsar to Azure Event Hubs", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Apache Pulsar to Azure Event Hubs for scalable and secure event streaming in Azure.", + "experimental": true + }, + { + "solutionId": "ibm-db2-to-azure-postgresql", + "name": "Migrate from IBM DB2 to Azure PostgreSQL", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from IBM DB2 to Azure PostgreSQL for scalable and secure database management in Azure.", + "experimental": true + }, + { + "solutionId": "firebird-to-azure-postgresql", + "name": "Migrate from Firebird to Azure PostgreSQL", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Firebird to Azure PostgreSQL for scalable and secure database management in Azure.", + "experimental": true + }, + { + "solutionId": "sqlite-to-azure-postgresql", + "name": "Migrate from SQLite to Azure PostgreSQL", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from SQLite to Azure PostgreSQL for scalable and secure database management in Azure.", + "experimental": true + }, + { + "solutionId": "google-cloud-functions-to-azure-functions", + "name": "Migrate from Google Cloud Functions to Azure Functions", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Google Cloud Functions to Azure Functions for scalable and secure serverless compute in Azure.", + "experimental": true + }, + { + "solutionId": "aws-lambda-to-azure-functions", + "name": "Migrate from AWS Lambda to Azure Functions", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from AWS Lambda to Azure Functions for scalable and secure serverless compute in Azure.", + "experimental": true + }, + { + "solutionId": "spring-batch-to-azure-durable-functions", + "name": "Migrate from Spring Batch to Azure Durable Functions", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Spring Batch to Azure Durable Functions for scalable and secure serverless compute in Azure.", + "experimental": true + }, + { + "solutionId": "google-cloud-storage-to-azure-blob-storage", + "name": "Migrate from Google Cloud Storage to Azure Blob Storage", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Google Cloud Storage to Azure Blob Storage for scalable and secure object storage in Azure.", + "experimental": true + }, + { + "solutionId": "amazon-sns-to-azure-service-bus", + "name": "Migrate from Amazon SNS to Azure Service Bus", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Amazon SNS to Azure Service Bus for scalable and secure messaging in Azure.", + "experimental": true + }, + { + "solutionId": "tibco-ems-jms-to-azure-service-bus", + "name": "Migrate from TIBCO EMS JMS to Azure Service Bus", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from TIBCO EMS JMS to Azure Service Bus for scalable and secure messaging in Azure.", + "experimental": true + }, + { + "solutionId": "solace-pubsub-to-azure-service-bus", + "name": "Migrate from Solace PubSub+ to Azure Service Bus", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Solace PubSub+ to Azure Service Bus for scalable and secure messaging in Azure.", + "experimental": true + }, + { + "solutionId": "amazon-kinesis-to-azure-event-hubs", + "name": "Migrate from Amazon Kinesis to Azure Event Hubs", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Migrate from Amazon Kinesis to Azure Event Hubs for scalable and secure event streaming in Azure.", + "experimental": true + }, + { + "solutionId": "azure-legacy-java-sdk-upgrade", + "name": "Upgrade from Legacy Azure SDKs for Java to the latest", + "type": "Formula", + "effort": "HIGH", + "tooltip": "Upgrade to the latest stable version of Azure SDKs for Java that follow current Azure SDK guidelines." + }, + { + "solutionId": "bare/weak-cryptography", + "name": "Replace Weak Cryptographic Algorithms", + "type": "Chat", + "prompt": "The application uses weak or broken cryptographic algorithms (such as MD5, SHA-1, DES, RC4, ECB mode, or Blowfish) that do not meet EU Cyber Resilience Act requirements. Replace weak hash algorithms with SHA-256 or SHA-3. Replace broken encryption (DES, RC4, Blowfish, ECB mode) with AES-GCM or ChaCha20-Poly1305. For password hashing, use bcrypt, Argon2, scrypt, or PBKDF2 instead of plain message digests.", + "tooltip": "The app uses weak cryptographic algorithms. Chat with Copilot to learn how to upgrade to modern, secure alternatives." + }, + { + "solutionId": "bare/insecure-tls", + "name": "Fix Insecure TLS/SSL Configuration", + "type": "Chat", + "prompt": "The application uses insecure TLS/SSL configurations, such as deprecated protocol versions (SSLv3, TLS 1.0, TLS 1.1), disabled certificate validation, disabled hostname verification, or weak cipher suites. Upgrade to TLS 1.2 or TLS 1.3, remove trust-all certificate patterns, ensure proper hostname verification, and use only strong cipher suites (AEAD modes like GCM or ChaCha20-Poly1305). For Spring Boot, set server.ssl.enabled-protocols=TLSv1.2,TLSv1.3.", + "tooltip": "The app has insecure TLS/SSL settings. Chat with Copilot to learn how to upgrade to secure TLS configurations." + }, + { + "solutionId": "bare/hardcoded-credentials", + "name": "Remove Hardcoded Credentials", + "type": "Chat", + "prompt": "The application contains hardcoded credentials (passwords, API keys, secrets, cryptographic keys, or default passwords) in source code or configuration files, violating EU Cyber Resilience Act secure-by-default requirements. Move all secrets to Azure Key Vault or a secrets management service. Use environment variables or externalized configuration for sensitive values. Use managed identities for service-to-service authentication. Ensure configuration files with secrets are excluded from version control.", + "tooltip": "The app has hardcoded credentials. Chat with Copilot to learn how to externalize secrets using Azure Key Vault or environment variables." + }, + { + "solutionId": "bare/insecure-random", + "name": "Use Cryptographically Secure Random Number Generation", + "type": "Chat", + "prompt": "The application uses insecure random number generators (java.util.Random, Math.random(), or ThreadLocalRandom) which are predictable and not suitable for security-sensitive operations such as token generation, session IDs, nonces, or encryption keys. Replace with java.security.SecureRandom for all security-relevant random number generation. SecureRandom provides a cryptographically strong random number generator (CSPRNG) backed by the OS entropy source.", + "tooltip": "The app uses insecure random number generators. Chat with Copilot to learn how to switch to SecureRandom for security-sensitive operations." + }, + { + "solutionId": "bare/aws-bedrock-to-azure-ai", + "name": "Migrate from AWS Bedrock to Azure OpenAI Service", + "type": "Chat", + "prompt": "The application uses AWS Bedrock SDK for generative AI capabilities. Consider migrating to Azure OpenAI Service or Azure AI Foundry. Replace AWS Bedrock SDK dependencies with the Azure OpenAI client library (com.azure:azure-ai-openai). Update application code to replace AWS Bedrock API calls with Azure OpenAI equivalents. Replace AWS IAM-based authentication with Azure AD managed identity or API key authentication using DefaultAzureCredential. Update configuration to replace AWS Bedrock settings (endpoint, model IDs, region) with Azure OpenAI configurations (endpoint, deployment name, API version). If using streaming APIs, refactor from AWS reactive streams pattern to Azure OpenAI's iterative streaming model.", + "tooltip": "The app uses AWS Bedrock for AI services. Chat with Copilot to learn how to migrate to Azure OpenAI Service or Azure AI Foundry." + } + ], + "rules": [ + { + "ruleId": "apm-00001", + "sourceCategory": "apm-newrelic", + "solution": "bare/apm-to-application-insights" + }, + { + "ruleId": "apm-00002", + "sourceCategory": "apm-elastic", + "solution": "bare/apm-to-application-insights" + }, + { + "ruleId": "apm-00003", + "sourceCategory": "apm-dynatrace", + "solution": "bare/apm-to-application-insights" + }, + { + "ruleId": "auth-00000", + "sourceCategory": "saml", + "solution": "on-premises-user-authentication-to-microsoft-entra-id" + }, + { + "ruleId": "auth-01000", + "sourceCategory": "opensaml", + "solution": "on-premises-user-authentication-to-microsoft-entra-id" + }, + { + "ruleId": "auth-02000", + "sourceCategory": "spring-security", + "solution": "on-premises-user-authentication-to-microsoft-entra-id" + }, + { + "ruleId": "auth-03000", + "sourceCategory": "oauth2", + "solution": "on-premises-user-authentication-to-microsoft-entra-id" + }, + { + "ruleId": "auth-04000", + "sourceCategory": "openid", + "solution": "on-premises-user-authentication-to-microsoft-entra-id" + }, + { + "ruleId": "webform-auth-00000", + "sourceCategory": "webform-auth", + "solution": "on-premises-user-authentication-to-microsoft-entra-id" + }, + { + "ruleId": "java-ldap-to-msft-entra-id-01000", + "solution": "on-premises-user-authentication-to-microsoft-entra-id" + }, + { + "ruleId": "azure-aws-config-credential-01000", + "sourceCategory": "aws-credentials", + "solution": "bare/aws-credentials-to-azure" + }, + { + "ruleId": "azure-aws-config-region-02000", + "sourceCategory": "aws-region-configuration", + "solution": "bare/aws-region-configuration-to-azure" + }, + { + "ruleId": "azure-aws-config-s3-03000", + "sourceCategory": "aws-s3", + "solution": "s3-to-azure-blob-storage" + }, + { + "ruleId": "azure-aws-config-s3-03001", + "sourceCategory": "aws-s3", + "solution": "s3-to-azure-blob-storage" + }, + { + "ruleId": "azure-aws-config-s3-03002", + "sourceCategory": "aws-s3", + "solution": "s3-to-azure-blob-storage" + }, + { + "ruleId": "azure-aws-config-secret-manager-05000", + "sourceCategory": "aws-secrets-manager", + "solution": "AWS-secrets-manager-to-azure-key-vault" + }, + { + "ruleId": "azure-aws-config-sqs-04000", + "sourceCategory": "aws-sqs", + "solution": "sqs-to-servicebus" + }, + { + "ruleId": "azure-aws-config-sqs-04001", + "sourceCategory": "aws-sqs", + "solution": "sqs-to-servicebus" + }, + { + "ruleId": "azure-aws-config-sqs-04002", + "sourceCategory": "aws-sqs", + "solution": "sqs-to-servicebus" + }, + { + "ruleId": "azure-aws-config-sqs-04003", + "sourceCategory": "aws-sqs", + "solution": "sqs-to-servicebus" + }, + { + "ruleId": "azure-aws-config-sqs-04004", + "sourceCategory": "aws-sqs", + "solution": "sqs-to-servicebus" + }, + { + "ruleId": "azure-cache-redis-01000", + "sourceCategory": "redis", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "azure-database-config-mongodb-02000", + "sourceCategory": "mongodb", + "solution": "mi-mongodb" + }, + { + "ruleId": "azure-database-microsoft-cassandra-04000", + "sourceCategory": "cassandra", + "solution": "mi-cassandra" + }, + { + "ruleId": "azure-database-microsoft-mariadb-06000", + "sourceCategory": "mariadb", + "solution": "mi-mariadb" + }, + { + "ruleId": "azure-database-microsoft-mongodb-05000", + "sourceCategory": "mongodb", + "solution": "mi-mongodb" + }, + { + "ruleId": "azure-database-microsoft-sql-03000", + "sourceCategory": "microsoft-sql", + "solution": "mi-azure-sql" + }, + { + "ruleId": "azure-database-mysql-01000", + "sourceCategory": "mysql", + "solution": "mi-mysql" + }, + { + "ruleId": "azure-database-postgresql-02000", + "sourceCategory": "postgresql", + "solution": "mi-postgresql" + }, + { + "ruleId": "azure-java-version-01000", + "solution": "java-version-upgrade" + }, + { + "ruleId": "azure-java-version-02000", + "solution": "java-version-upgrade" + }, + { + "ruleId": "azure-keystore-certificates-01000", + "solution": "certificate-management-to-azure-key-vault" + }, + { + "ruleId": "azure-keystore-certificates-02000", + "solution": "certificate-management-to-azure-key-vault" + }, + { + "ruleId": "dockerfile-00000", + "solution": "containerization-copilot-agent" + }, + { + "ruleId": "dockerfile-00010", + "solution": "containerization-copilot-agent" + }, + { + "ruleId": "dockerfile-00020", + "solution": "containerization-copilot-agent" + }, + { + "ruleId": "dockerfile-00030", + "solution": "containerization-copilot-agent" + }, + { + "ruleId": "azure-message-queue-activemq-01000", + "sourceCategory": "activemq-artemis", + "solution": "activemq-servicebus" + }, + { + "ruleId": "azure-message-queue-amqp-02000", + "sourceCategory": "spring-amqp-rabbitmq", + "solution": "amqp-rabbitmq-servicebus" + }, + { + "ruleId": "azure-message-queue-config-artemis-01000", + "sourceCategory": "activemq-artemis", + "solution": "activemq-servicebus" + }, + { + "ruleId": "azure-message-queue-config-kafka-01000", + "sourceCategory": "kafka", + "solution": "confluent-cloud-kafka" + }, + { + "ruleId": "azure-message-queue-config-rabbitmq-01000", + "sourceCategory": "spring-amqp-rabbitmq", + "solution": "amqp-rabbitmq-servicebus" + }, + { + "ruleId": "azure-message-queue-rabbitmq-01000", + "sourceCategory": "spring-amqp-rabbitmq", + "solution": "amqp-rabbitmq-servicebus" + }, + { + "ruleId": "azure-message-queue-java-ee-rabbitmq-amqp-01000", + "sourceCategory": "java-ee-amqp-rabbitmq", + "solution": "java-ee-amqp-rabbitmq-servicebus" + }, + { + "ruleId": "azure-message-queue-spring-jms-rabbitmq-01000", + "sourceCategory": "spring-jms-rabbitmq", + "solution": "spring-jms-rabbitmq-servicebus" + }, + { + "ruleId": "azure-message-queue-ibm-jms-01000", + "sourceCategory": "jms-ibm-mq", + "solution": "ibm-mq-jms-to-azure-service-bus" + }, + { + "ruleId": "azure-password-01000", + "solution": "plaintext-credential-to-azure-keyvault" + }, + { + "ruleId": "azure-system-config-01000", + "sourceCategory": "environment-variables", + "solution": "bare/configuration-management/environment-variables" + }, + { + "ruleId": "external-config-00000", + "sourceCategory": "external-configuration", + "solution": "bare/configuration-management/external-configuration" + }, + { + "ruleId": "windows-registry-00000", + "sourceCategory": "windows-registry", + "solution": "bare/configuration-management/windows-registry" + }, + { + "ruleId": "azure-tas-binding-01000", + "sourceCategory": "tanzu-application-service", + "solution": "bare/azure-service-connector" + }, + { + "ruleId": "clustering-00000", + "sourceCategory": "http-session", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "eap-to-azure-appservice-datasource-driver-01000", + "sourceCategory": "jboss-eap", + "solution": "bare/eap-migration/jboss-eap" + }, + { + "ruleId": "eap-to-azure-appservice-pom-001", + "sourceCategory": "jboss-eap", + "solution": "bare/eap-migration/jboss-eap" + }, + { + "ruleId": "embedded-cache-01000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-02000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-03000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-04000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-05000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-06000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-07000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-08000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-09000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-10000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-11000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-12000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-13000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-14000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-15000", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "embedded-cache-16000", + "sourceCategory": "redis", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "hardcoded-ip-address", + "sourceCategory": "hardcode-ip", + "solution": "bare/remote-communication/hardcode-ip" + }, + { + "ruleId": "unsecure-network-protocol-00000", + "sourceCategory": "secure-protocols", + "solution": "bare/remote-communication/secure-protocols" + }, + { + "ruleId": "hardcoded-urls-00001", + "sourceCategory": "hardcoded-urls", + "solution": "bare/remote-communication/hardcoded-urls" + }, + { + "ruleId": "hardcoded-urls-00002", + "sourceCategory": "hardcoded-urls", + "solution": "bare/remote-communication/hardcoded-urls" + }, + { + "ruleId": "java-corba-00000", + "sourceCategory": "corba", + "solution": "bare/remote-communication/corba" + }, + { + "ruleId": "java-removals-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-removals-00010", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-rmi-00000", + "sourceCategory": "rmi", + "solution": "bare/jakataee-to-azure/rmi" + }, + { + "ruleId": "java-rmi-00001", + "sourceCategory": "rmi", + "solution": "bare/jakataee-to-azure/rmi" + }, + { + "ruleId": "java-rpc-00000", + "solution": "jax-rpc-to-jax-ws" + }, + { + "ruleId": "jca-00000", + "sourceCategory": "jca", + "solution": "bare/jakataee-to-azure/jca" + }, + { + "ruleId": "jni-native-code-00000", + "sourceCategory": "jni-native-code", + "solution": "bare/java-native-code" + }, + { + "ruleId": "jni-native-code-00001", + "sourceCategory": "jni-native-code", + "solution": "bare/java-native-code" + }, + { + "ruleId": "azure-file-system-02000", + "sourceCategory": "local-file-system", + "solution": "local-files-to-mounted-azure-storage" + }, + { + "ruleId": "azure-file-system-03000", + "sourceCategory": "local-file-system", + "solution": "local-files-to-mounted-azure-storage" + }, + { + "ruleId": "local-storage-00001", + "sourceCategory": "local-file-system", + "solution": "local-files-to-mounted-azure-storage" + }, + { + "ruleId": "local-storage-00002", + "sourceCategory": "local-file-system", + "solution": "local-files-to-mounted-azure-storage" + }, + { + "ruleId": "local-storage-00003", + "sourceCategory": "local-file-system", + "solution": "local-files-to-mounted-azure-storage" + }, + { + "ruleId": "local-storage-00004", + "sourceCategory": "local-file-system", + "solution": "local-files-to-mounted-azure-storage" + }, + { + "ruleId": "local-storage-00005", + "sourceCategory": "local-file-system", + "solution": "local-files-to-mounted-azure-storage" + }, + { + "ruleId": "local-storage-00006", + "sourceCategory": "local-file-system", + "solution": "local-files-to-mounted-azure-storage" + }, + { + "ruleId": "localhost-http-00001", + "sourceCategory": "localhost", + "solution": "bare/local-resource-access" + }, + { + "ruleId": "localhost-jdbc-00002", + "sourceCategory": "localhost", + "solution": "bare/local-resource-access" + }, + { + "ruleId": "localhost-ws-00003", + "sourceCategory": "localhost", + "solution": "bare/local-resource-access" + }, + { + "ruleId": "localhost-00004", + "sourceCategory": "localhost", + "solution": "bare/local-resource-access" + }, + { + "ruleId": "logging-0000", + "solution": "log-to-console" + }, + { + "ruleId": "logging-0001", + "solution": "log-to-console" + }, + { + "ruleId": "logging-0002", + "solution": "log-to-console" + }, + { + "ruleId": "logging-0004", + "sourceCategory": "splunk", + "solution": "log-to-console" + }, + { + "ruleId": "logging-0005", + "sourceCategory": "zipkin", + "solution": "bare/apm-to-application-insights" + }, + { + "ruleId": "lombok-incompatibility-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "mail-00000", + "sourceCategory": "java-mail", + "solution": "javax.email-send-to-azure-communication-service-email" + }, + { + "ruleId": "os-specific-00002", + "solution": "bare/os-compatibility" + }, + { + "ruleId": "removed-packages-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "removed-packages-00010", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "session-00001", + "sourceCategory": "http-session", + "solution": "other-cache-solutions-to-azure-managed-cache" + }, + { + "ruleId": "socket-communication-00000", + "sourceCategory": "java-socket", + "solution": "bare/remote-communication/java-socket" + }, + { + "ruleId": "socket-communication-00001", + "sourceCategory": "java-socket", + "solution": "bare/remote-communication/java-socket" + }, + { + "ruleId": "spring-boot-to-azure-config-server-01000", + "sourceCategory": "spring-cloud", + "solution": "spring-cloud-config-to-azure-app-configuration" + }, + { + "ruleId": "spring-boot-to-azure-eureka-01000", + "sourceCategory": "spring-cloud", + "solution": "bare/spring-migration" + }, + { + "ruleId": "spring-boot-to-azure-eureka-02000", + "sourceCategory": "spring-cloud", + "solution": "bare/spring-migration" + }, + { + "ruleId": "spring-boot-to-azure-eureka-03000", + "sourceCategory": "spring-cloud", + "solution": "bare/spring-migration" + }, + { + "ruleId": "spring-boot-to-azure-key-vault-01000", + "sourceCategory": "spring-cloud-vault", + "solution": "bare/spring-cloud-vault-migration" + }, + { + "ruleId": "spring-boot-to-azure-openfeign-01000", + "sourceCategory": "spring-cloud", + "solution": "bare/spring-migration" + }, + { + "ruleId": "spring-boot-to-azure-port-01000", + "sourceCategory": "spring-cloud", + "solution": "bare/spring-migration" + }, + { + "ruleId": "spring-boot-to-azure-restricted-config-01000", + "sourceCategory": "spring-cloud", + "solution": "bare/spring-migration" + }, + { + "ruleId": "spring-boot-to-azure-spring-boot-version-01000", + "sourceCategory": "spring-boot", + "solution": "spring-boot-upgrade" + }, + { + "ruleId": "spring-boot-to-azure-spring-cloud-version-01000", + "sourceCategory": "spring-cloud", + "solution": "spring-boot-upgrade" + }, + { + "ruleId": "spring-boot-to-azure-spring-cloud-version-02000", + "sourceCategory": "spring-cloud", + "solution": "spring-boot-upgrade" + }, + { + "ruleId": "spring-framework-version-01000", + "sourceCategory": "spring-framework", + "solution": "spring-framework-upgrade" + }, + { + "ruleId": "jakarta-ee-version-01000", + "sourceCategory": "java-ee/jakarta-ee", + "solution": "jakarta-ee-upgrade" + }, + { + "ruleId": "utf-8-by-default-00000", + "solution": "bare/encoding-standards" + }, + { + "ruleId": "utf-8-by-default-00010", + "solution": "bare/encoding-standards" + }, + { + "ruleId": "utf-8-by-default-00020", + "solution": "bare/encoding-standards" + }, + { + "ruleId": "utf-8-by-default-00030", + "solution": "bare/encoding-standards" + }, + { + "ruleId": "web-10000", + "sourceCategory": "javax-swing", + "solution": "bare/redesign-java-gui-app" + }, + { + "ruleId": "web-11000", + "sourceCategory": "javafx", + "solution": "bare/redesign-java-gui-app" + }, + { + "ruleId": "oracle2openjdk-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "oracle2openjdk-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "oracle2openjdk-00002", + "solution": "bare/oraclejdk-to-openjdk/resource-management-apis" + }, + { + "ruleId": "oracle2openjdk-00003", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "oracle2openjdk-00004", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "oracle2openjdk-00005", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "oracle2openjdk-00006", + "solution": "bare/oraclejdk-to-openjdk/imageio" + }, + { + "ruleId": "java-8-deprecate-apt-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-callback-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-corba-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-javafx-builder-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-log-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-odbc-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-pack-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-pack-00002", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-security-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-security-00002", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-security-manager-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-security-manager-00002", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-stream-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-8-deprecate-thread-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-10-deprecate-dom-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-10-deprecate-javafx-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-10-deprecate-runtime-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-10-deprecate-security-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-10-deprecate-security-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-10-deprecate-security-00002", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-10-deprecate-security-00003", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-10-deprecate-security-00004", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-awt-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-corba-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-javaee-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-javaee-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-pack-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-peer-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-property-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-property-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-security-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-security-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-stream-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-thread-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-unsafe-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-11-deprecate-unsafe-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-agent-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-dom-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-javafx-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-log-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-pack-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-peer-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-property-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-property-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-reflect-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-reflect-00010", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-security-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-security-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-security-00002", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-tracing-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-unsafe-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-unsafe-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-9-deprecate-url-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-removals-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-removals-00010", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-12-deprecate-finalize-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-12-deprecate-finalize-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-12-deprecate-security-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-13-deprecate-runtime-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-13-deprecate-security-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-14-deprecate-pack-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-14-deprecate-property-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-14-deprecate-property-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-14-deprecate-security-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-14-deprecate-thread-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-15-deprecate-property-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-15-deprecate-signer-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-15-deprecate-ssl-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-15-deprecate-ssl-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-16-deprecate-security-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-16-deprecate-thread-group-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-17-deprecate-applet-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-17-deprecate-security-manager-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-17-deprecate-security-manager-00010", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-17-deprecate-security-manager-00020", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-17-deprecate-security-manager-00030", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-17-deprecate-security-manager-00040", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-17-deprecate-security-manager-00050", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-17-deprecate-security-manager-00060", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-17-deprecate-security-manager-00070", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-17-deprecate-socket-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-17-deprecate-unsafe-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "lombok-incompatibility-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "removed-packages-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "removed-packages-00010", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-18-deprecate-finalize-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-18-deprecate-finalize-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-18-deprecate-property-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-18-deprecate-runtime-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-18-deprecate-security-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-18-deprecate-socket-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-18-deprecate-thread-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-18-deprecate-unsafe-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-19-deprecate-locale-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-19-deprecate-param-spec-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-19-deprecate-param-spec-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-19-deprecate-class-00010", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-19-deprecate-thread-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-19-deprecate-thread-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-19-deprecate-thread-00002", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-20-deprecate-jmx-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-20-deprecate-net-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-20-deprecate-thread-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-20-deprecate-thread-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-20-deprecate-thread-00002", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-21-deprecate-dynamic-agents-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-21-deprecate-file-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-21-deprecate-file-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-21-deprecate-jmx-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-21-deprecate-jmx-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-21-deprecate-property-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-21-deprecate-property-00001", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-21-deprecate-signer-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "java-21-deprecate-thread-00000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "openliberty-database-00001", + "sourceCategory": "openliberty-database", + "solution": "bare/openliberty-migration/openliberty-database" + }, + { + "ruleId": "openliberty-database-00002", + "sourceCategory": "openliberty-database", + "solution": "bare/openliberty-migration/openliberty-database" + }, + { + "ruleId": "openliberty-database-00003", + "sourceCategory": "openliberty-database", + "solution": "bare/openliberty-migration/openliberty-database" + }, + { + "ruleId": "openliberty-database-00004", + "sourceCategory": "openliberty-database", + "solution": "bare/openliberty-migration/openliberty-database" + }, + { + "ruleId": "openliberty-database-00005", + "sourceCategory": "openliberty-database", + "solution": "bare/openliberty-migration/openliberty-database" + }, + { + "ruleId": "openliberty-database-00006", + "sourceCategory": "openliberty-database", + "solution": "bare/openliberty-migration/openliberty-database" + }, + { + "ruleId": "openliberty-database-00007", + "sourceCategory": "openliberty-database", + "solution": "bare/openliberty-migration/openliberty-database" + }, + { + "ruleId": "openliberty-database-00008", + "sourceCategory": "openliberty-database", + "solution": "bare/openliberty-migration/openliberty-database" + }, + { + "ruleId": "openliberty-database-00009", + "sourceCategory": "openliberty-database", + "solution": "bare/openliberty-migration/openliberty-database" + }, + { + "ruleId": "openliberty-filesystem-00001", + "sourceCategory": "openliberty-filesystem", + "solution": "bare/openliberty-migration/openliberty-filesystem" + }, + { + "ruleId": "openliberty-filesystem-00002", + "sourceCategory": "openliberty-filesystem", + "solution": "bare/openliberty-migration/openliberty-filesystem" + }, + { + "ruleId": "openliberty-filesystem-00003", + "sourceCategory": "openliberty-filesystem", + "solution": "bare/openliberty-migration/openliberty-filesystem" + }, + { + "ruleId": "openliberty-filesystem-00004", + "sourceCategory": "openliberty-filesystem", + "solution": "bare/openliberty-migration/openliberty-filesystem" + }, + { + "ruleId": "openliberty-jms-00001", + "sourceCategory": "openliberty-jms", + "solution": "bare/openliberty-migration/openliberty-jms" + }, + { + "ruleId": "openliberty-jms-00002", + "sourceCategory": "openliberty-jms", + "solution": "bare/openliberty-migration/openliberty-jms" + }, + { + "ruleId": "openliberty-jms-00003", + "sourceCategory": "openliberty-jms", + "solution": "bare/openliberty-migration/openliberty-jms" + }, + { + "ruleId": "openliberty-jms-00004", + "sourceCategory": "openliberty-jms", + "solution": "bare/openliberty-migration/openliberty-jms" + }, + { + "ruleId": "openliberty-jms-00005", + "sourceCategory": "openliberty-jms", + "solution": "bare/openliberty-migration/openliberty-jms" + }, + { + "ruleId": "openliberty-jms-00006", + "sourceCategory": "openliberty-jms", + "solution": "bare/openliberty-migration/openliberty-jms" + }, + { + "ruleId": "openliberty-logging-00001", + "sourceCategory": "openliberty-logging", + "solution": "bare/openliberty-migration/openliberty-logging" + }, + { + "ruleId": "openliberty-logging-00002", + "sourceCategory": "openliberty-logging", + "solution": "bare/openliberty-migration/openliberty-logging" + }, + { + "ruleId": "azure-database-microsoft-oracle-07000", + "sourceCategory": "oracle", + "solution": "oracle-to-postgresql" + }, + { + "ruleId": "database-reliability-01000", + "solution": "bare/database-migration/database-reliability" + }, + { + "ruleId": "eclipse-00002", + "sourceCategory": "eclipse", + "solution": "eclipse-project-to-maven-project" + }, + { + "ruleId": "ant-build-tool-00001", + "sourceCategory": "ant", + "solution": "ant-project-to-maven-project" + }, + { + "ruleId": "google-pubsub-to-azure-service-bus-01000", + "sourceCategory": "google-pubsub", + "solution": "google-cloud-pub-sub-to-azure-service-bus" + }, + { + "ruleId": "google-gcr-to-azure-acr-01000", + "sourceCategory": "google-gcr", + "solution": "google-gcr-to-azure-acr" + }, + { + "ruleId": "sybase-ase-to-azure-database-01000", + "sourceCategory": "sybase-ase", + "solution": "sybase-ase-to-azure-postgresql" + }, + { + "ruleId": "google-firestore-to-azure-cosmosdb-01000", + "sourceCategory": "google-firestore", + "solution": "google-firestore-to-azure-cosmos-db" + }, + { + "ruleId": "google-cloud-bigtable-to-azure-cosmosdb-01000", + "sourceCategory": "google-cloud-bigtable", + "solution": "google-cloud-bigtable-to-azure-cosmos-db" + }, + { + "ruleId": "google-cloud-spanner-to-azure-postgresql-01000", + "sourceCategory": "google-cloud-spanner", + "solution": "google-cloud-spanner-to-azure-postgresql" + }, + { + "ruleId": "apache-pulsar-to-azure-eventhubs-01000", + "sourceCategory": "apache-pulsar", + "solution": "apache-pulsar-to-azure-event-hubs" + }, + { + "ruleId": "ibm-db2-to-azure-postgresql-01000", + "sourceCategory": "ibm-db2", + "solution": "ibm-db2-to-azure-postgresql" + }, + { + "ruleId": "firebird-to-azure-postgresql-01000", + "sourceCategory": "firebird", + "solution": "firebird-to-azure-postgresql" + }, + { + "ruleId": "sqlite-to-azure-postgresql-01000", + "sourceCategory": "sqlite", + "solution": "sqlite-to-azure-postgresql" + }, + { + "ruleId": "google-cloud-functions-to-azure-functions-01000", + "sourceCategory": "google-cloud-functions", + "solution": "google-cloud-functions-to-azure-functions" + }, + { + "ruleId": "aws-lambda-to-azure-functions-01000", + "sourceCategory": "aws-lambda", + "solution": "aws-lambda-to-azure-functions" + }, + { + "ruleId": "quartz-scheduler-to-azure-functions-01000", + "sourceCategory": "quartz-scheduler", + "solution": "quartz-scheduler-to-azure-functions" + }, + { + "ruleId": "spring-batch-to-azure-durable-functions-01000", + "sourceCategory": "spring-batch", + "solution": "spring-batch-to-azure-durable-functions" + }, + { + "ruleId": "google-cloud-storage-to-azure-blob-storage-01000", + "sourceCategory": "google-cloud-storage", + "solution": "google-cloud-storage-to-azure-blob-storage" + }, + { + "ruleId": "amazon-sns-to-azure-servicebus-01000", + "sourceCategory": "amazon-sns", + "solution": "amazon-sns-to-azure-service-bus" + }, + { + "ruleId": "tibco-ems-jms-to-azure-servicebus-jms-01000", + "sourceCategory": "tibco-ems-jms", + "solution": "tibco-ems-jms-to-azure-service-bus" + }, + { + "ruleId": "solace-pubsubplus-to-azure-servicebus-01000", + "sourceCategory": "solace-pubsubplus", + "solution": "solace-pubsub-to-azure-service-bus" + }, + { + "ruleId": "amazon-kinesis-to-azure-eventhubs-01000", + "sourceCategory": "amazon-kinesis", + "solution": "amazon-kinesis-to-azure-event-hubs" + }, + { + "ruleId": "jakarta-auth-00001", + "sourceCategory": "jakarta-auth", + "solution": "bare/jakarta-auth-migration" + }, + { + "ruleId": "jakarta-database-00001", + "sourceCategory": "jakarta-nosql", + "solution": "bare/jakarta-nosql-migration" + }, + { + "ruleId": "jakarta-database-00002", + "sourceCategory": "jakarta-persistence", + "solution": "bare/jakarta-persistence-migration" + }, + { + "ruleId": "jakarta-database-00003", + "sourceCategory": "jakarta-data", + "solution": "bare/jakarta-data-migration" + }, + { + "ruleId": "jakarta-service-00001", + "sourceCategory": "jakarta-websocket", + "solution": "bare/jakarta-websocket-migration" + }, + { + "ruleId": "jakarta-service-00002", + "sourceCategory": "jakarta-jaxrs", + "solution": "bare/jakarta-jaxrs-migration" + }, + { + "ruleId": "websphere-to-azure-app-service", + "sourceCategory": "websphere-to-azure-app-service", + "solution": "bare/websphere-to-azure-app-service" + }, + { + "ruleId": "websphere-to-aks", + "sourceCategory": "websphere-to-aks", + "solution": "bare/websphere-to-aks" + }, + { + "ruleId": "websphere-to-azure-container-apps", + "sourceCategory": "websphere-to-azure-container-apps", + "solution": "bare/websphere-to-azure-container-apps" + }, + { + "ruleId": "weblogic-to-azure-app-service", + "sourceCategory": "weblogic-to-azure-app-service", + "solution": "bare/weblogic-to-azure-app-service" + }, + { + "ruleId": "weblogic-to-aks", + "sourceCategory": "weblogic-to-aks", + "solution": "bare/weblogic-to-aks" + }, + { + "ruleId": "weblogic-to-azure-container-apps", + "sourceCategory": "weblogic-to-azure-container-apps", + "solution": "bare/weblogic-to-azure-container-apps" + }, + { + "ruleId": "jboss-eap-to-azure-app-service", + "sourceCategory": "jboss-eap-to-azure-app-service", + "solution": "bare/jboss-eap-to-azure-app-service" + }, + { + "ruleId": "jboss-eap-to-aks", + "sourceCategory": "jboss-eap-to-aks", + "solution": "bare/jboss-eap-to-aks" + }, + { + "ruleId": "jboss-eap-to-azure-container-apps", + "sourceCategory": "jboss-eap-to-azure-container-apps", + "solution": "bare/jboss-eap-to-azure-container-apps" + }, + { + "ruleId": "jakarta-cdi-00002", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "jakarta-cdi-00003", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "jboss-dependencies-00006", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "jboss-eap5-7-java-03000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "jboss-eap5-7-java-08000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "jboss-eap4and5to6and7-java-01000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "jboss-eap5and6to7-java-08000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "base64-01000", + "solution": "deprecated-api-upgrade" + }, + { + "ruleId": "seam-java-00010", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "seam-java-00040", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "seam-java-00070", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "seam-java-00030", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "seam-java-00080", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "commonj-weblogic-02000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "commonj-weblogic-03000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "commonj-weblogic-05000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "commonj-weblogic-06000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "commonj-weblogic-07000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "weblogic-jms-eap7-01000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "weblogic-portability-lifecycle-01000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "weblogic-portability-servlet-01000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "weblogic-webservices-eap7-01000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "weblogic-webapp-eap7-07000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "commonj-websphere-02000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "commonj-websphere-03000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "commonj-websphere-05000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "commonj-websphere-06000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "commonj-websphere-07000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "websphere-jms-eap7-01000", + "solution": "bare/appserver-api-migration-to-standard-java" + }, + { + "ruleId": "azure-java-sdk-legacy-migration-01000", + "solution": "azure-legacy-java-sdk-upgrade" + }, + { + "ruleId": "cra-weak-crypto-md5-01000", + "solution": "bare/weak-cryptography" + }, + { + "ruleId": "cra-weak-crypto-sha1-02000", + "solution": "bare/weak-cryptography" + }, + { + "ruleId": "cra-weak-crypto-des-03000", + "solution": "bare/weak-cryptography" + }, + { + "ruleId": "cra-weak-crypto-rc4-04000", + "solution": "bare/weak-cryptography" + }, + { + "ruleId": "cra-weak-crypto-ecb-05000", + "solution": "bare/weak-cryptography" + }, + { + "ruleId": "cra-weak-crypto-blowfish-06000", + "solution": "bare/weak-cryptography" + }, + { + "ruleId": "cra-weak-crypto-password-hash-07000", + "solution": "bare/weak-cryptography" + }, + { + "ruleId": "cra-insecure-tls-protocol-01000", + "solution": "bare/insecure-tls" + }, + { + "ruleId": "cra-insecure-tls-config-02000", + "solution": "bare/insecure-tls" + }, + { + "ruleId": "cra-insecure-tls-trust-all-03000", + "solution": "bare/insecure-tls" + }, + { + "ruleId": "cra-insecure-tls-hostname-verify-04000", + "solution": "bare/insecure-tls" + }, + { + "ruleId": "cra-insecure-tls-cipher-suite-05000", + "solution": "bare/insecure-tls" + }, + { + "ruleId": "cra-hardcoded-credential-password-01000", + "solution": "bare/hardcoded-credentials" + }, + { + "ruleId": "cra-hardcoded-credential-apikey-02000", + "solution": "bare/hardcoded-credentials" + }, + { + "ruleId": "cra-hardcoded-credential-config-03000", + "solution": "bare/hardcoded-credentials" + }, + { + "ruleId": "cra-hardcoded-credential-default-pwd-04000", + "solution": "bare/hardcoded-credentials" + }, + { + "ruleId": "cra-hardcoded-credential-crypto-key-05000", + "solution": "bare/hardcoded-credentials" + }, + { + "ruleId": "cra-insecure-random-01000", + "solution": "bare/insecure-random" + }, + { + "ruleId": "cra-insecure-random-math-02000", + "solution": "bare/insecure-random" + }, + { + "ruleId": "cra-insecure-random-threadlocal-03000", + "solution": "bare/insecure-random" + }, + { + "ruleId": "aws-bedrock-to-azure-ai-06000", + "sourceCategory": "aws-bedrock", + "solution": "bare/aws-bedrock-to-azure-ai" + }, + { + "ruleId": "aws-bedrock-to-azure-ai-06001", + "sourceCategory": "aws-bedrock", + "solution": "bare/aws-bedrock-to-azure-ai" + }, + { + "ruleId": "aws-bedrock-to-azure-ai-06002", + "sourceCategory": "aws-bedrock", + "solution": "bare/aws-bedrock-to-azure-ai" + } + ] +} diff --git a/plugins/github-copilot-modernization/skills/assessment-report-converter/tests/test_report_tools.py b/plugins/github-copilot-modernization/skills/assessment-report-converter/tests/test_report_tools.py new file mode 100644 index 0000000..9467fa3 --- /dev/null +++ b/plugins/github-copilot-modernization/skills/assessment-report-converter/tests/test_report_tools.py @@ -0,0 +1,571 @@ +#!/usr/bin/env python3 +"""CLI tests for the assessment-report-converter helper scripts. + +The skill ships two interchangeable implementations of the same helper CLI: + + * ``scripts/report_tools.sh`` — bash + jq + * ``scripts/report_tools.ps1`` — PowerShell 7+ + +Both MUST behave identically (same output data, same exit codes: 0 valid, +1 invalid, 2 read/parse error). This harness drives whichever interpreters are +available on the machine and runs the full assertion battery against each one, +skipping an interpreter that is not installed. There is no Python implementation +any more, so every check goes through a subprocess. + +Run from anywhere with stdlib only:: + + python -m unittest discover -s skills/assessment-report-converter/tests -v + +On Linux/CI ``bash``+``jq`` and ``pwsh`` are both present. On Windows ``bash`` +is the WSL launcher (paths are converted to ``/mnt/...``) and ``pwsh`` is +PowerShell 7. These tests are developer-only and are excluded from the shipped +plugin via ``copilot-cli-plugin/.syncignore``. +""" + +import json +import os +import shutil +import subprocess +import tempfile +import unittest + +HERE = os.path.dirname(os.path.abspath(__file__)) +SCRIPTS_DIR = os.path.abspath(os.path.join(HERE, "..", "scripts")) +SH_PATH = os.path.join(SCRIPTS_DIR, "report_tools.sh") +PS1_PATH = os.path.join(SCRIPTS_DIR, "report_tools.ps1") +SCHEMA_PATH = os.path.join(SCRIPTS_DIR, "assessment-report.schema.json") +IS_WIN = os.name == "nt" + + +# --------------------------------------------------------------------------- # +# Path helper (Windows -> WSL) +# --------------------------------------------------------------------------- # +def _win_to_wsl(path): + """Convert C:\\a\\b to /mnt/c/a/b without spawning wslpath.""" + drive, rest = os.path.splitdrive(os.path.abspath(path)) + return "/mnt/" + drive[0].lower() + rest.replace("\\", "/") + + +def _shq(value): + return "'" + value.replace("'", "'\\''") + "'" + + +# --------------------------------------------------------------------------- # +# Interpreter runners — each exposes .run(*args) and .path(p) +# --------------------------------------------------------------------------- # +class _Runner: + name = "?" + + def run(self, *args): + raise NotImplementedError + + def path(self, p): + return p + + +class BashRunner(_Runner): + name = "bash" + + def __init__(self): + self.script = _win_to_wsl(SH_PATH) if IS_WIN else SH_PATH + + @staticmethod + def available(): + if not shutil.which("bash"): + return False + probe = subprocess.run( + ["bash", "-lc", "command -v jq >/dev/null 2>&1 && echo JQ_OK"], + capture_output=True, text=True, + ) + return "JQ_OK" in probe.stdout + + def path(self, p): + return _win_to_wsl(p) if IS_WIN else p + + def run(self, *args): + if IS_WIN: + inner = " ".join(["bash", _shq(self.script)] + [_shq(a) for a in args]) + return subprocess.run(["bash", "-lc", inner], capture_output=True, text=True) + return subprocess.run(["bash", self.script, *args], capture_output=True, text=True) + + +class PwshRunner(_Runner): + name = "pwsh" + + def __init__(self): + # Require PowerShell 7+ (`pwsh`): the script uses `ConvertTo-Json -AsArray`, + # which Windows PowerShell 5.1 (`powershell.exe`) does not support. + self.exe = shutil.which("pwsh") + + @staticmethod + def available(): + return bool(shutil.which("pwsh")) + + def run(self, *args): + return subprocess.run( + [self.exe, "-NoProfile", "-File", PS1_PATH, *args], + capture_output=True, text=True, + ) + + +def _discover_runners(): + runners = [] + if BashRunner.available(): + runners.append(BashRunner()) + if PwshRunner.available(): + runners.append(PwshRunner()) + return runners + + +RUNNERS = _discover_runners() + + +# --------------------------------------------------------------------------- # +# Mock data +# --------------------------------------------------------------------------- # +def valid_report(): + """A minimal report that passes the structural + consistency checks. + + Tests deep-copy this via json round-trip and mutate a single field to + exercise one failure at a time, so every negative test stays isolated. + """ + return { + "version": "1.0.0", + "producer": "CSV import", + "metadata": { + "id": "report-test-001", + "name": "Test Report", + "status": "completed", + "analysisStartTime": "2026-01-01T00:00:00Z", + "mode": "full", + "domains": ["java-upgrade", "security"], + "targetIds": ["azure-appservice"], + }, + "projects": [ + { + "path": "app", + "properties": {"appName": "demo-app"}, + "incidents": [ + { + "ruleId": "spring-boot-upgrade", + "incidentId": "inc-1", + "location": "pom.xml", + "locationKind": "file", + "line": 12, + "column": 3, + } + ], + } + ], + "rules": { + "spring-boot-upgrade": { + "id": "spring-boot-upgrade", + "title": "Upgrade Spring Boot to a supported version", + "severity": "mandatory", + "effort": 5, + "domain": "java-upgrade", + "category": "spring-boot", + } + }, + "security": [ + { + "id": "CVE-2024-0001", + "title": "Vulnerable dependency", + "category": "dependency-vulnerability", + "severity": "mandatory", + "description": "A vulnerable library version is in use.", + "storyPoint": 3, + "evidence": { + "files": ["pom.xml"], + "explanation": "commons-x 1.0 is affected.", + }, + } + ], + } + + +def _clone(report): + return json.loads(json.dumps(report)) + + +def _load_schema(): + with open(SCHEMA_PATH, "r", encoding="utf-8") as fh: + return json.load(fh) + + +# --------------------------------------------------------------------------- # +# Subprocess helpers +# --------------------------------------------------------------------------- # +def _run_validate(runner, report_obj): + """Write report_obj to a temp file, validate it, return (rc, stdout, errors).""" + fd, path = tempfile.mkstemp(suffix=".json") + with os.fdopen(fd, "w", encoding="utf-8") as fh: + json.dump(report_obj, fh) + try: + cp = runner.run("validate", runner.path(path)) + finally: + os.remove(path) + errors = [line[4:] for line in cp.stdout.splitlines() if line.startswith(" - ")] + return cp.returncode, cp.stdout, errors + + +@unittest.skipUnless(RUNNERS, "no report_tools interpreter (bash+jq or pwsh) available") +class _MultiInterpreterCase(unittest.TestCase): + """Base class: subclasses iterate their body over every available runner.""" + + def for_each_runner(self): + for runner in RUNNERS: + yield runner + + +# --------------------------------------------------------------------------- # +# Deterministic lookups (against the real solution-mapping.json) +# --------------------------------------------------------------------------- # +class TestLookups(_MultiInterpreterCase): + def test_upgrade_solutions_resolves_all_components(self): + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + cp = runner.run("upgrade-solutions") + self.assertEqual(cp.returncode, 0, cp.stderr) + data = json.loads(cp.stdout) + for component in ("jdk", "spring-boot", "spring-framework", "jakarta-ee"): + self.assertIn(component, data) + self.assertTrue(data[component]["preferredRuleId"], + f"{component} needs a ruleId") + + def test_list_solutions_ids_only(self): + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + cp = runner.run("list-solutions", "--type", "Formula", "--ids-only") + self.assertEqual(cp.returncode, 0, cp.stderr) + ids = [line for line in cp.stdout.splitlines() if line.strip()] + self.assertGreater(len(ids), 0) + + def test_list_solutions_query_filters(self): + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + cp = runner.run("list-solutions", "--query", "spring") + self.assertEqual(cp.returncode, 0, cp.stderr) + selected = json.loads(cp.stdout) + self.assertTrue(selected, "expected at least one 'spring' solution") + for sol in selected: + haystack = " ".join( + str(sol.get(k, "")) for k in ("solutionId", "name", "tooltip") + ).lower() + self.assertIn("spring", haystack) + + def test_list_solutions_no_match_is_empty_array(self): + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + cp = runner.run("list-solutions", "--query", "zzz-no-such-solution") + self.assertEqual(cp.returncode, 0, cp.stderr) + self.assertEqual(json.loads(cp.stdout), []) + + def test_rules_for_known_solution(self): + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + cp = runner.run("rules-for-solution", "spring-boot-upgrade") + self.assertEqual(cp.returncode, 0, cp.stderr) + data = json.loads(cp.stdout) + self.assertEqual(data["solutionId"], "spring-boot-upgrade") + self.assertGreater(data["ruleCount"], 0) + self.assertTrue(data["preferredRuleId"]) + self.assertEqual(data["rules"][0]["ruleId"], data["preferredRuleId"]) + + def test_rules_for_unknown_solution_is_empty(self): + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + cp = runner.run("rules-for-solution", "does-not-exist") + self.assertEqual(cp.returncode, 0, cp.stderr) + data = json.loads(cp.stdout) + self.assertEqual(data["ruleCount"], 0) + self.assertEqual(data["rules"], []) + self.assertIsNone(data["preferredRuleId"]) + + +# --------------------------------------------------------------------------- # +# validate — structural + cross-field checks (one failure per test) +# --------------------------------------------------------------------------- # +class TestValidate(_MultiInterpreterCase): + def assertHasError(self, errors, needle): + self.assertTrue( + any(needle in e for e in errors), + f"expected an error containing {needle!r}; got: {errors}", + ) + + def _assert_invalid_with(self, mutate, needle): + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + report = valid_report() + mutate(report) + rc, stdout, errors = _run_validate(runner, report) + self.assertEqual(rc, 1, stdout) + self.assertIn("RESULT: INVALID", stdout) + self.assertHasError(errors, needle) + + def test_valid_report_is_valid(self): + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + rc, stdout, errors = _run_validate(runner, valid_report()) + self.assertEqual(rc, 0, stdout) + self.assertIn("RESULT: VALID", stdout) + self.assertEqual(errors, []) + + def test_report_must_be_object(self): + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + rc, stdout, errors = _run_validate(runner, ["not", "an", "object"]) + self.assertEqual(rc, 1, stdout) + self.assertHasError(errors, "expected a JSON object") + + def test_missing_top_level_field(self): + self._assert_invalid_with(lambda r: r.pop("rules"), + 'missing required field "rules"') + + def test_invalid_metadata_status(self): + def m(r): + r["metadata"]["status"] = "done" + self._assert_invalid_with(m, "metadata.status") + + def test_invalid_metadata_mode(self): + def m(r): + r["metadata"]["mode"] = "partial" + self._assert_invalid_with(m, "metadata.mode") + + def test_invalid_domain_enum(self): + def m(r): + r["metadata"]["domains"] = ["performance"] + self._assert_invalid_with(m, "metadata.domains: invalid value") + + def test_rule_invalid_severity(self): + def m(r): + r["rules"]["spring-boot-upgrade"]["severity"] = "critical" + self._assert_invalid_with(m, "severity: invalid value") + + def test_rule_negative_effort(self): + def m(r): + r["rules"]["spring-boot-upgrade"]["effort"] = -1 + self._assert_invalid_with(m, "effort: must be an integer >= 0") + + def test_rule_invalid_domain(self): + def m(r): + r["rules"]["spring-boot-upgrade"]["domain"] = "networking" + self._assert_invalid_with(m, "domain: invalid value") + + def test_rule_missing_field(self): + def m(r): + del r["rules"]["spring-boot-upgrade"]["category"] + self._assert_invalid_with(m, 'missing required field "category"') + + def test_project_properties_missing_appname(self): + def m(r): + r["projects"][0]["properties"] = {} + self._assert_invalid_with(m, 'properties: missing required field "appName"') + + def test_incident_missing_field(self): + def m(r): + del r["projects"][0]["incidents"][0]["locationKind"] + self._assert_invalid_with(m, 'missing required field "locationKind"') + + def test_incident_ruleid_referential_integrity(self): + def m(r): + r["projects"][0]["incidents"][0]["ruleId"] = "ghost-rule" + self._assert_invalid_with(m, "has no matching entry in rules{}") + + def test_incident_line_below_one(self): + def m(r): + r["projects"][0]["incidents"][0]["line"] = 0 + self._assert_invalid_with(m, "line: must be an integer >= 1") + + def test_security_invalid_severity(self): + def m(r): + r["security"][0]["severity"] = "information" # not in the 3-value security scale + self._assert_invalid_with(m, "severity: invalid value") + + def test_security_duplicate_id(self): + def m(r): + r["security"].append(_clone(r["security"][0])) + self._assert_invalid_with(m, "is duplicated") + + def test_security_evidence_missing_field(self): + def m(r): + del r["security"][0]["evidence"]["explanation"] + self._assert_invalid_with(m, 'evidence: missing required field "explanation"') + + def test_domains_declares_security_but_none_present(self): + def m(r): + r["security"] = [] + self._assert_invalid_with(m, "report.security is empty") + + def test_security_present_but_domain_not_declared(self): + def m(r): + r["metadata"]["domains"] = ["java-upgrade"] + self._assert_invalid_with(m, 'does not include "security"') + + def test_missing_file_exit_two(self): + ghost = os.path.join(tempfile.gettempdir(), "no-such-report-xyz.json") + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + cp = runner.run("validate", runner.path(ghost)) + self.assertEqual(cp.returncode, 2, cp.stdout + cp.stderr) + + def test_malformed_json_exit_two(self): + fd, path = tempfile.mkstemp(suffix=".json") + with os.fdopen(fd, "w", encoding="utf-8") as fh: + fh.write("{ not: valid json ") + try: + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + cp = runner.run("validate", runner.path(path)) + self.assertEqual(cp.returncode, 2, cp.stdout + cp.stderr) + finally: + os.remove(path) + + +# --------------------------------------------------------------------------- # +# Schema-sync guard (behavioral): the scripts hard-code the schema's enums. If +# the schema drifts, these tests fail so both scripts are updated to match — the +# schema is the source of truth. We probe `validate` with a universe of tokens +# and assert the set the script *accepts* for each field equals the schema enum. +# --------------------------------------------------------------------------- # +class TestSchemaEnumSync(_MultiInterpreterCase): + @classmethod + def setUpClass(cls): + cls.schema = _load_schema() + cls.defs = cls.schema.get("definitions", {}) + cls.meta_props = cls.schema["properties"]["metadata"]["properties"] + + def _accepted(self, runner, universe, mutate, needle): + """Return the subset of `universe` the script does NOT flag with needle.""" + accepted = set() + for value in universe: + report = valid_report() + mutate(report, value) + _, _, errors = _run_validate(runner, report) + if not any(needle in e for e in errors): + accepted.add(value) + return accepted + + def test_rule_severity_enum_matches_schema(self): + schema_enum = set(self.defs["Severity"]["enum"]) + universe = schema_enum | {"critical", "high", "low", "info"} + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + accepted = self._accepted( + runner, universe, + lambda r, v: r["rules"]["spring-boot-upgrade"].__setitem__("severity", v), + "rules[spring-boot-upgrade].severity: invalid value", + ) + self.assertEqual(accepted, schema_enum) + + def test_security_severity_enum_matches_schema(self): + schema_enum = set(self.defs["SecurityFinding"]["properties"]["severity"]["enum"]) + # "information" is valid for rules but NOT for security — a good discriminator. + universe = schema_enum | {"information", "critical", "high"} + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + accepted = self._accepted( + runner, universe, + lambda r, v: r["security"][0].__setitem__("severity", v), + "security[0].severity: invalid value", + ) + self.assertEqual(accepted, schema_enum) + + def test_status_enum_matches_schema(self): + schema_enum = set(self.meta_props["status"]["enum"]) + universe = schema_enum | {"done", "open", "closed"} + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + accepted = self._accepted( + runner, universe, + lambda r, v: r["metadata"].__setitem__("status", v), + "metadata.status: invalid value", + ) + self.assertEqual(accepted, schema_enum) + + def test_mode_enum_matches_schema(self): + schema_enum = set(self.meta_props["mode"]["enum"]) + universe = schema_enum | {"partial", "quick"} + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + accepted = self._accepted( + runner, universe, + lambda r, v: r["metadata"].__setitem__("mode", v), + "metadata.mode: invalid value", + ) + self.assertEqual(accepted, schema_enum) + + def test_domain_enum_matches_schema(self): + schema_enum = set(self.meta_props["domains"]["items"]["enum"]) + # Rule.domain must share the same vocabulary. + self.assertEqual(set(self.defs["Rule"]["properties"]["domain"]["enum"]), schema_enum) + universe = schema_enum | {"performance", "networking"} + + def mutate(r, v): + r["metadata"]["domains"] = [v] + + for runner in self.for_each_runner(): + with self.subTest(interp=runner.name): + accepted = self._accepted( + runner, universe, mutate, + "metadata.domains: invalid value", + ) + self.assertEqual(accepted, schema_enum) + + +# --------------------------------------------------------------------------- # +# Cross-interpreter parity: bash and pwsh must report identical validate errors. +# --------------------------------------------------------------------------- # +@unittest.skipUnless(len(RUNNERS) >= 2, "need both bash and pwsh for parity check") +class TestInterpreterParity(unittest.TestCase): + def _errors(self, runner, report): + _, _, errors = _run_validate(runner, report) + return errors + + def test_valid_report_identical(self): + report = valid_report() + base = self._errors(RUNNERS[0], report) + for other in RUNNERS[1:]: + self.assertEqual(self._errors(other, report), base) + + def test_multi_error_report_identical(self): + report = valid_report() + report["metadata"]["status"] = "done" # status enum + report["rules"]["spring-boot-upgrade"]["severity"] = "critical" # rule severity + report["projects"][0]["incidents"][0]["ruleId"] = "ghost-rule" # dangling ref + report["security"][0]["severity"] = "information" # security severity + base = self._errors(RUNNERS[0], report) + self.assertTrue(base) + for other in RUNNERS[1:]: + self.assertEqual(set(self._errors(other, report)), set(base)) + + def test_missing_field_plus_invalid_value_identical(self): + # An object that is BOTH missing a required field AND carries an invalid + # value must report both errors on every interpreter. This exercises the + # path where a naive validator could short-circuit after the missing-field + # error and skip the value checks (bash concatenates; pwsh must too). + report = valid_report() + + rule = report["rules"]["spring-boot-upgrade"] + del rule["category"] # missing required field + rule["domain"] = "networking" # + invalid enum on the same object + + finding = report["security"][0] + del finding["title"] # missing required field + finding["severity"] = "critical" # + invalid enum on the same object + del finding["evidence"] # missing object whose sub-fields must still be reported + + base = self._errors(RUNNERS[0], report) + # Every interpreter must surface the missing-field AND the value errors. + self.assertIn('rules[spring-boot-upgrade]: missing required field "category"', base) + self.assertTrue(any("rules[spring-boot-upgrade].domain: invalid value" in e for e in base)) + self.assertIn('security[0]: missing required field "title"', base) + self.assertTrue(any("security[0].severity: invalid value" in e for e in base)) + self.assertIn('security[0]: missing required field "evidence"', base) + self.assertTrue(any("security[0].evidence: missing required field" in e for e in base)) + for other in RUNNERS[1:]: + self.assertEqual(set(self._errors(other, report)), set(base)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/plugins/github-copilot-modernization/skills/create-modernization-plan/SKILL.md b/plugins/github-copilot-modernization/skills/create-modernization-plan/SKILL.md index 5b33d17..0c92d02 100644 --- a/plugins/github-copilot-modernization/skills/create-modernization-plan/SKILL.md +++ b/plugins/github-copilot-modernization/skills/create-modernization-plan/SKILL.md @@ -52,7 +52,7 @@ Given the user input, do this: 1) Follow the structure of the selected template to generate the plan 2) Follow the rules defined in the template to fill in the sections with relevant information based on the analysis of user input and content of mentioned files 3) Save the plan in folder ${modernization-work-folder} with the filename plan.md. If a plan already exists, overwrite it. - 4) Generate a separate tasks.json file following the tasks-schema.json schema with setupBaseline, infrastructure, upgrade, transform, containerization, and deployment tasks + 4) Generate a separate tasks.json file following the tasks-schema.json schema with setupBaseline, infrastructure, upgrade, transform, integration test, containerization, and deployment tasks 5) Save the tasks in folder ${modernization-work-folder}/.metadata/ with the filename tasks.json. If tasks.json already exists, overwrite it. **Clarification Outcomes in Plan**: Incorporate all clarification answers from steps 3–4 into `plan.md` and `tasks.json`: @@ -77,10 +77,28 @@ Given the user input, do this: - You MUST NOT use the pattern name as the skill name in the generated plan and tasks.json. - If there are similar skills defined in project skill `.github/skills/` versus other skills, MUST use the one defined in project. - Skills must be fully matched. For migration scenarios, both the source product and target product must match the task intent. - - Each task should be independently testable + - Each task should be independently testable with integration tests - Do not add tests for unimpacted code or existing functionality unless user requested - **IMPORTANT**: Do NOT read individual skill files at this stage; Do Not include the skill detail in the tasks. + **Integration Test Task Rules**: Add an integration test task when EITHER of these conditions is met: + 1. The user explicitly requests integration testing (e.g., "add integration tests", "generate integration tests", "test the migration") + 2. The user answers the Integration Testing questionnaire question with any option OTHER than "No — skip integration testing entirely" (including when a default option is inferred because an environment is provided/provisioned) + + When an integration test task is included: + - Add an integration test task with type "integrationTest" after all transform/upgrade tasks but before containerization tasks + - This integration test task should: + - Have id format: "{sequence}-integrationTest" where sequence is the next number after the last migration task (e.g., if last migration is 001, use "002-integrationTest") + - Have description: "Build integration tests for migrated Azure services and run post-migration verification" + - Have dependencies on ALL of: setupBaseline task ID, infrastructure task ID (if present), and ALL transform/upgrade task IDs. The integrationTest task is the convergence point that waits for all parallel work to complete. + - Do NOT store resource IDs, subscription IDs, or connection strings in the task plan. If user provides infra info (resource ID, subscription ID, connection strings), record it in `./infra/infra-config.md`. + + **Baseline Task Rules**: A setupBaseline task is **mandatory whenever an integrationTest task is included** in the plan. + - **Parallel execution**: The setupBaseline task and infrastructure task run in **parallel** with no dependencies between them. The setupBaseline task snapshots the source folder and operates on the snapshot, so it is not affected by concurrent code changes or infra provisioning. Set `snapshotFolder` to the project's main source directory (relative to project root). + - **Transform/upgrade tasks run sequentially**: Upgrade and transform tasks MUST be chained with dependencies (each depends on the previous one) to avoid file conflicts from concurrent code modifications. However, they run in parallel with setupBaseline and infrastructure since they modify different concerns. + - **Dependencies**: The setupBaseline task should have NO dependencies (empty `dependencies` array or omit it). Upgrade/transform tasks depend on the previous upgrade/transform task in sequence. Only the `integrationTest` verification task depends on ALL of: setupBaseline, infrastructure (if present), and all transform/upgrade tasks completing. + - **Purpose**: setupBaseline produces the frozen test specification (test-cases, testdata) and the **infra-decision-table** — the real/mock strategy for every external dependency used by integration tests. This decision is frozen into the baseline bundle and reused as-is by the verification phase. + **Java Upgrade Task Guidelines**: Only add an upgrade task if the user explicitly requests it. You must refer to the ./java-upgrade-guideline.md for specific rules and guidelines when creating Java upgrade tasks. **.NET Upgrade Task Guidelines**: You must refer to the ./dotnet-upgrade-guideline.md for specific rules and guidelines when creating .NET upgrade tasks. diff --git a/plugins/github-copilot-modernization/skills/create-modernization-plan/questionnaire.md b/plugins/github-copilot-modernization/skills/create-modernization-plan/questionnaire.md index 8a87b16..042ac16 100644 --- a/plugins/github-copilot-modernization/skills/create-modernization-plan/questionnaire.md +++ b/plugins/github-copilot-modernization/skills/create-modernization-plan/questionnaire.md @@ -11,6 +11,33 @@ Should the plan include environment/infrastructure provisioning? * Yes — provision new infrastructure * Custom — use an existing externally managed environment specified by the user instead of provisioning or repo-defined configuration (ask for resource group, subscription, environment name, or config path) +## Integration Testing + +Should the plan include integration testing to verify migrated services? + +- (Default when infrastructure is provided/provisioned) Yes — Real mode: use provisioned infrastructure from `infra/` or user-provided environment +- (Default when no infrastructure is provided) Yes — Mock mode: use mocked dependencies +- Yes — TestContainer mode: use containerized emulators/dependencies for supported services and mock unsupported dependencies +- Yes — Mixed mode: user-specified per dependency (ask for dependency list and mode per dependency) +- No — skip integration testing entirely + +### Integration Test Resource Info (only ask when Real mode is selected AND user chose to use existing infrastructure) + +If the user selected Real mode testing with an existing environment (i.e., "Custom" environment in the Environment Setup section above), ask for one of the following resource information: + +* Azure Resource ID — the full Azure resource ID (e.g., `/subscriptions/{sub-id}/resourceGroups/{rg}/providers/...`) for the target resource(s) the tests will run against +* Subscription ID and Resource Group — the resource group containing the test infrastructure + +Record these in `./infra/infra-config.md`. + +### Subscription ID for Provisioning (only ask when user chose to provision new test infrastructure) + +If the user selected "Yes" in the Environment Setup section (provision new infrastructure), ask: + +* Azure Subscription ID — the subscription where test infrastructure should be provisioned + +Record this in the infrastructure task's `environmentConfiguration` field. The InfrastructureExpert agent will use it during provisioning and persist the resulting resource info to its private user profile. + ## Security & CVE Remediation Should the plan include a security scan and CVE remediation task? This task runs after all upgrade and transform tasks and before deployment to identify and fix known vulnerabilities. diff --git a/plugins/github-copilot-modernization/skills/create-modernization-plan/summary-schema.json b/plugins/github-copilot-modernization/skills/create-modernization-plan/summary-schema.json new file mode 100644 index 0000000..edd0880 --- /dev/null +++ b/plugins/github-copilot-modernization/skills/create-modernization-plan/summary-schema.json @@ -0,0 +1,136 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Run Summary", + "description": "Schema for .metadata/summary.json — the post-run goal-status document produced by the team while finalizing tasks. Each entry corresponds to one task in tasks.json (joined by 'id'). The CLI reads this file to render the post-run summary tables (per-task goal/result, overall application status, reference documents).", + "type": "object", + "additionalProperties": false, + "required": ["version", "taskSummaries"], + "properties": { + "$schema": { "type": "string", "description": "Optional schema URI." }, + "version": { "type": "string", "description": "Schema version. Use \"1.0\"." }, + "taskSummaries": { + "type": "array", + "description": "One entry per task in tasks.json that has reached a terminal state. Joined to tasks.json by 'id'.", + "items": { "$ref": "#/$defs/runSummaryEntry" } + } + }, + "$defs": { + "runSummaryEntry": { + "type": "object", + "additionalProperties": false, + "required": ["id", "type", "goalStatus"], + "description": "A per-task goal-status entry. The shape of 'goalStatus' is selected by 'type'.", + "properties": { + "id": { "type": "string", "description": "Task id matching the corresponding task in tasks.json." }, + "type": { + "type": "string", + "enum": ["setupBaseline", "upgrade", "transform", "security", "integrationTest", "infrastructure", "containerization", "deployment"], + "description": "Task type discriminator matching the corresponding task type in tasks.json." + }, + "goalStatus": { + "description": "Structured goal status object. Use the per-type shape selected by 'type' from the matching $defs entry below." + }, + "risks": { + "type": "array", + "maxItems": 3, + "items": { "type": "string", "maxLength": 200 }, + "description": "Up to 3 short, concrete residual risks introduced or left behind by this task (e.g., 'Spring upgrade still uses deprecated WebSecurityConfigurerAdapter'). Omit or use [] when there are none. Do not invent risks to fill space; do not include generic platitudes." + }, + "followUps": { + "type": "array", + "maxItems": 3, + "items": { "type": "string", "maxLength": 200 }, + "description": "Up to 3 short, actionable items the user/team should do next (e.g., 'Pin docker base image digest', 'Add IT coverage for OAuth refresh path'). Omit or use [] when there are none." + } + }, + "allOf": [ + { "if": { "properties": { "type": { "const": "setupBaseline" } } }, "then": { "properties": { "goalStatus": { "$ref": "#/$defs/setupBaselineGoalStatus" } } } }, + { "if": { "properties": { "type": { "const": "upgrade" } } }, "then": { "properties": { "goalStatus": { "$ref": "#/$defs/upgradeGoalStatus" } } } }, + { "if": { "properties": { "type": { "const": "transform" } } }, "then": { "properties": { "goalStatus": { "$ref": "#/$defs/transformGoalStatus" } } } }, + { "if": { "properties": { "type": { "const": "security" } } }, "then": { "properties": { "goalStatus": { "$ref": "#/$defs/securityGoalStatus" } } } }, + { "if": { "properties": { "type": { "const": "integrationTest" } } }, "then": { "properties": { "goalStatus": { "$ref": "#/$defs/integrationTestGoalStatus" } } } }, + { "if": { "properties": { "type": { "const": "infrastructure" } } }, "then": { "properties": { "goalStatus": { "$ref": "#/$defs/infrastructureGoalStatus" } } } }, + { "if": { "properties": { "type": { "const": "containerization" } } }, "then": { "properties": { "goalStatus": { "$ref": "#/$defs/containerizationGoalStatus" } } } }, + { "if": { "properties": { "type": { "const": "deployment" } } }, "then": { "properties": { "goalStatus": { "$ref": "#/$defs/deploymentGoalStatus" } } } } + ] + }, + "setupBaselineGoalStatus": { + "type": "object", + "additionalProperties": false, + "description": "Goal status for a setupBaseline task. Populate when the task reaches a terminal state so the post-run summary can render concrete counts and link to the produced test-cases.md.", + "properties": { + "totalTestCases": { "type": "integer", "description": "Total number of test cases captured in the baseline spec." }, + "passed": { "type": "integer", "description": "Number of baseline test cases that passed at the end of the run." }, + "failed": { "type": "integer", "description": "Number of baseline test cases that failed at the end of the run." }, + "allCasesPassed": { "type": "boolean", "description": "Convenience boolean: whether all captured test cases passed at the end of the run. Equivalent to failed == 0 when passed/failed are both populated." }, + "testCasesFile": { "type": "string", "description": "Workspace-relative path to the produced test-cases.md file, forward-slash separated (e.g., 'src/test/test-cases/test-cases.md')." } + } + }, + "upgradeGoalStatus": { + "type": "object", + "additionalProperties": false, + "description": "Goal status for an upgrade task. Populate when the task reaches a terminal state.", + "properties": { + "fromVersion": { "type": "string", "description": "Source version observed before the upgrade, e.g., 'Java 8', '.NET 6'." }, + "targetVersion": { "type": "string", "description": "Target version reached, e.g., 'Java 17', '.NET 8'." } + } + }, + "transformGoalStatus": { + "type": "object", + "additionalProperties": false, + "description": "Goal status for a transform task. Populate when the task reaches a terminal state.", + "properties": { + "migrationFrom": { "type": "string", "description": "Source component being replaced, e.g., 'RabbitMQ', 'AWS S3'." }, + "migrationTo": { "type": "string", "description": "Azure destination component, e.g., 'Azure Service Bus', 'Azure Blob Storage'." } + } + }, + "securityGoalStatus": { + "type": "object", + "additionalProperties": false, + "description": "Goal status for a security task. Populate when the task reaches a terminal state.", + "properties": { + "cvesFixed": { "type": "integer", "description": "Number of CVEs fixed during this task." }, + "cvesRemaining": { "type": "integer", "description": "Number of CVEs that remain unfixed after this task." } + } + }, + "integrationTestGoalStatus": { + "type": "object", + "additionalProperties": false, + "description": "Goal status for an integrationTest task. Populate when the task reaches a terminal state.", + "properties": { + "totalTestCases": { "type": "integer", "description": "Total number of integration test cases executed." }, + "passed": { "type": "integer", "description": "Number of integration test cases that passed." }, + "failed": { "type": "integer", "description": "Number of integration test cases that failed." }, + "testCasesFile": { "type": "string", "description": "Workspace-relative path to the consumed test-cases.md file, forward-slash separated." } + } + }, + "infrastructureGoalStatus": { + "type": "object", + "additionalProperties": false, + "description": "Goal status for an infrastructure task. Populate when the task reaches a terminal state.", + "properties": { + "provisioned": { "type": "boolean", "description": "Whether Azure resources were successfully provisioned." }, + "resourceGroup": { "type": "string", "description": "Name of the Azure resource group used for provisioning." } + } + }, + "containerizationGoalStatus": { + "type": "object", + "additionalProperties": false, + "description": "Goal status for a containerization task. Populate when the task reaches a terminal state.", + "properties": { + "imageBuilt": { "type": "boolean", "description": "Whether the container image was successfully built." }, + "imageTag": { "type": "string", "description": "Built container image tag, e.g., 'myapp:1.0.0'." } + } + }, + "deploymentGoalStatus": { + "type": "object", + "additionalProperties": false, + "description": "Goal status for a deployment task. Populate when the task reaches a terminal state.", + "properties": { + "deployed": { "type": "boolean", "description": "Whether the application was successfully deployed to the target Azure service." }, + "resourceGroup": { "type": "string", "description": "Name of the Azure resource group the application was deployed into." }, + "accessUrl": { "type": "string", "description": "Public URL or endpoint the user can use to access the deployed application." } + } + } + } +} diff --git a/plugins/github-copilot-modernization/skills/create-modernization-plan/tasks-schema.json b/plugins/github-copilot-modernization/skills/create-modernization-plan/tasks-schema.json index ad2c378..2ed3b4c 100644 --- a/plugins/github-copilot-modernization/skills/create-modernization-plan/tasks-schema.json +++ b/plugins/github-copilot-modernization/skills/create-modernization-plan/tasks-schema.json @@ -21,10 +21,12 @@ "oneOf": [ { "$ref": "#/$defs/transformTask" }, { "$ref": "#/$defs/upgradeTask" }, + { "$ref": "#/$defs/integrationTestTask" }, { "$ref": "#/$defs/containerizationTask" }, { "$ref": "#/$defs/deploymentTask" }, { "$ref": "#/$defs/securityTask" }, - { "$ref": "#/$defs/infrastructureTask" } + { "$ref": "#/$defs/infrastructureTask" }, + { "$ref": "#/$defs/setupBaselineTask" } ] } }, @@ -267,6 +269,22 @@ } ] }, + "integrationTestTask": { + "allOf": [ + { "$ref": "#/$defs/taskBase" }, + { + "type": "object", + "additionalProperties": false, + "required": ["type"], + "properties": { + "type": { + "const": "integrationTest", + "description": "Integration test task template - Generate and run integration tests for migrated Azure services. Only include when user explicitly requests integration testing. This task runs after all transform/upgrade tasks but before containerization." + } + } + } + ] + }, "securityTask": { "allOf": [ { "$ref": "#/$defs/taskBase" }, @@ -318,6 +336,28 @@ } } ] + }, + "setupBaselineTask": { + "allOf": [ + { "$ref": "#/$defs/taskBase" }, + { + "type": "object", + "additionalProperties": false, + "required": ["type"], + "properties": { + "type": { + "const": "setupBaseline", + "description": "Setup baseline task template - Establish the modernization baseline by capturing the current state of the application before any changes are made." + }, + "snapshotFolder": { + "type": "string", + "description": "Project source folder to snapshot before baseline analysis, relative to the project root." + }, + "successCriteria": { "$ref": "#/$defs/successCriteria" }, + "successCriteriaStatus": { "$ref": "#/$defs/successCriteriaStatus" } + } + } + ] } } } diff --git a/plugins/github-copilot-modernization/skills/create-test-baseline/SKILL.md b/plugins/github-copilot-modernization/skills/create-test-baseline/SKILL.md new file mode 100644 index 0000000..087f04c --- /dev/null +++ b/plugins/github-copilot-modernization/skills/create-test-baseline/SKILL.md @@ -0,0 +1,176 @@ +--- +name: create-test-baseline +description: Create a test baseline for the project to be modernized. The baseline will be used for later verification of modernization tasks. +--- + +# Goal + +Produce a precise, executable-quality **specification** of the application's externally observable behavior that must be preserved across migration. The output is `test-cases/test-cases.md` plus the `test-cases/testdata/` fixtures it references — **no test code is generated in this phase**. The `verify-test-baseline` skill consumes this spec to generate `*PostMigrationIT` tests against the new implementation. + +## User Input + +- **migration-scope**: The scope of the migration, e.g. "migrate from AWS S3 to Azure Blob Storage", "upgrade from Java 8 to Java 11" etc. +- **taskid**: Identifier for this baseline run, used to namespace the summary report. +- **modernization-work-folder**: Folder under which the summary report is written. + +## Principles + +- The spec describes behavior at **external boundaries only** — HTTP endpoints, CLI commands, public API surfaces, published/consumed events, message queues, scheduled tasks, webhooks. No service-specific SDK types, no backend implementation details. +- The spec must be **precise enough to be transformed into test code mechanically**. Vague phrases ("should return a reasonable error", "behaves as expected") are defects, not descriptions. +- All payload data is **externalized** under `test-cases/testdata/` as files. The spec references them by path (relative to `test-cases/`). No inline byte literals, no hand-built JSON strings in the spec. +- `test-cases/test-cases.md` and `test-cases/testdata/` are **FROZEN** after this skill completes — never modified, renamed, moved, or deleted by subsequent phases. +- **No `*BaselineIT` test code is produced.** The migration uses a replace strategy: any code that exercises the old implementation would either import old SDKs (which get deleted) or substitute internal services for the real entry point (which defeats the purpose). All executable tests are produced in the verify phase against the new implementation. +- **The module is the test-case boundary.** In a multi-module project, each module that is in scope produces its own independent `test-cases.md` under that module's test source root. A module's `test-cases.md` MUST contain only test cases whose entry points belong to that module. Never place test cases for one module inside another module's `test-cases.md` (e.g. API-gateway module test cases must not appear in a backend-services module's spec). +- **Skip modules with no migration-relevant resource access.** If a module's code does not directly interact with any resource being migrated (e.g. it never calls Azure/AWS/GCP SDKs, never accesses a database or message broker that is part of the migration scope), do not create `test-cases/` for that module. A module that only serves as a thin HTTP frontend delegating to another module's backend services — without touching any migrated resource itself — does not need test cases. +- **Mock cross-module and non-migration-related dependencies.** When a module's entry point calls a service provided by another module (e.g. a shared service in a sibling module) or an external service unrelated to the migration scope, mark that dependency as **mock** in the infra decision table. The `verify-test-baseline` skill handles the actual mock implementation. +- **Respect existing integration tests.** Scan for existing IT / integration / E2E tests before writing new test cases. Do not duplicate coverage they already provide — reference them and fill gaps only. New test cases MUST follow the same conventions (naming, framework, assertions, fixtures, helpers) observed in existing tests. + +## Output Layout + +Frozen baseline artifacts live under a dedicated `test-cases/` subdirectory of the project's **test source root** — the standard directory the build tool already uses for tests (e.g. `src/test/` for Maven/Gradle Java modules, `tests/` for Python / Node / Go projects, `/test/` for multi-module repos). In a multi-module repo, place artifacts under each module's own test source root. Do NOT invent a new top-level folder. + +The per-run summary report lives under the modernization work folder, not the test source root. + +``` +/ +└── test-cases/ # FROZEN — entire folder is the baseline spec bundle + ├── test-cases.md # FROZEN — the full behavioral spec + ├── infra-decision-table.md # FROZEN — mock/real/testcontainer decision per external dependency (Step 2) + └── testdata/ # FROZEN — fixtures referenced by test-cases.md + ├── inputs/ # raw input files (images, JSON payloads, CSV, etc.) + ├── expectations/ # golden outputs at the application boundary + └── ... # other data as needed (configuration, seed data, etc.) + +${modernization-work-folder}/${taskid}/ +└── baseline-summary.md # NEW — per-run summary (Step 6) +``` + +All paths inside `test-cases.md` (e.g. `testdata/inputs/sample.jpg`) are interpreted **relative to `/test-cases/`**, the folder that contains `test-cases.md`. + +## Workflow + +### Step 1: Inventory External Boundaries and Orchestration Entry Points + +Scan the production source for everything that constitutes an external boundary or orchestration entry point. Record each one with file path, symbol, and **owning module** so it can be cross-checked in Step 2. + +**What qualifies as an entry point** (principle, not a closed list): + +An entry point is any code location invoked by something **outside the application's own call graph** — the network, the OS, the runtime scheduler, a message broker, an external SDK consumer, etc. If the application does not call it itself, it is an entry point. + +Typical examples (use as hints, not as an exhaustive checklist — apply the principle above to whatever the codebase actually uses): + +- Network-facing handlers (HTTP / REST / gRPC / GraphQL controllers, routers, webhook receivers) +- Process-level entries (CLI commands, `main` methods, background workers) +- Runtime-invoked callbacks (scheduled / cron jobs, framework-triggered lifecycle hooks) +- Broker-driven consumers (message-queue listeners, streaming / pub-sub subscribers) +- Container-invoked enterprise bean entry points (EJB remote/local business methods, message-driven beans) +- Published library / SDK methods — public methods that are never called from within the same module (i.e. only invoked by external consumers) + +Every orchestration entry point in the migration scope MUST be covered by at least one **end-to-end** test case that triggers it with realistic input and verifies the final observable outcome. Test cases that exercise only helpers called *within* an entry point do NOT count toward this requirement. + +**Multi-module scoping:** Group entry points by owning module. For each module, determine whether it directly accesses any resource being migrated (databases, message brokers, cloud storage, caches, etc. that are in the migration scope). Modules whose code never directly interacts with a migration-relevant resource are **out of scope** — do not produce test cases for them. Record the per-module decision (in-scope / out-of-scope with reason) so it is auditable in the baseline summary. + +### Step 2: Inventory Existing Integration Tests + +Scan the project's test source roots for existing IT / integration / E2E tests. Map each existing test to the entry points inventoried in Step 1 and note which coverage categories (happy-path, boundary, special-input, failure) it covers. + +Also extract the project's testing conventions: naming patterns, test framework and assertion style, fixture/test-data organization, helper utilities, and infrastructure setup (Testcontainers, embedded servers, mocks, etc.). These conventions are binding — the `verify-test-baseline` skill MUST follow them when generating test code. + +Include the existing test inventory and extracted conventions in the baseline summary (Step 7). + +### Step 3: Write `test-cases.md` + +For each **in-scope module** (as determined in Step 1), produce `/test-cases/test-cases.md` using [test-cases-template.md](test-cases-template.md). Each module gets its own independent spec file containing only test cases for entry points that belong to that module. + +**Existing test alignment rules:** +- If an existing test already covers an entry point + category, mark it as `covered-by-existing` in the spec with the test's fully qualified name. Do NOT duplicate it. +- New test cases fill coverage gaps only and MUST follow the conventions extracted in Step 2. + +**Module isolation rules:** +- A module's `test-cases.md` MUST NOT contain test cases for entry points defined in other modules. +- When an entry point in module A calls a service in module B, note the cross-module dependency in the test case's `Preconditions` field (e.g. "Service B returns X"). The infra decision table marks it as **mock**; the `verify-test-baseline` skill handles the actual mock implementation. +- External services unrelated to the migration scope (third-party APIs, internal microservices outside the project) are noted as dependencies and marked **mock** in the infra decision table. + +For each entry point, cover the **four coverage buckets**: + +1. **Happy path** — typical valid input, normal outcome. +2. **Boundary values** — empty, max-size, page boundaries, off-by-one cases. +3. **Special inputs** — unicode, reserved characters, missing referenced resources, idempotency keys. +4. **Failure mapping** — simulated backend / dependency failure → application-level response. + +Use **2–5 representative records per entity** (table row, queue message, container object, etc.). + +**Every test case MUST have all required fields populated** — see "Required field checklist" below. Missing or vague fields make the spec unverifiable and must be filled in before freezing. + +### Step 4: Externalize Test Data + +For every payload referenced in `test-cases.md`, create a file under `/test-cases/testdata/` and reference it from the spec as `testdata/...` (i.e. relative to `/test-cases/`). + +**Requirements:** +- Organize by purpose: `inputs/`, `expectations/`, `configuration/`, `seed-data/`. +- If existing tests already use fixture files, prefer reusing them or following the same directory structure and naming conventions. Copy or reference existing fixtures under `testdata/` rather than inventing a parallel layout. +- No inline byte literals, hardcoded keys, or hand-built JSON strings in `test-cases.md`. Every `Input` and `Expected Output` block either references a file or contains a small structured value (status code, exit code, scalar string) that does not warrant a file. +- File names should be descriptive and stable: `sample.jpg`, `upload-request.json`, `error-not-found.json`. + +### Step 5: Build Infra Decision Table (mock vs real vs testcontainer) + +With the full set of test cases and their referenced dependencies now visible, decide which external dependencies the post-migration tests will exercise as **real** resources, **testcontainer** resources, or **mock** at the SDK / HTTP boundary. This decision is recorded once here and is reused as-is by `verify-test-baseline` — verification does not re-decide. + +**Inputs:** +- The exhaustive list of external dependencies actually touched by the test cases written in Step 3 (cross-checked against the entry-point inventory from Step 1). +- The mock/real/testcontainer decisions already made by existing tests (from Step 2). Prefer consistency with existing tests unless there is a clear reason to diverge. +- The repo-root `infra/` directory (`*.md`, `*.yml`, `*.yaml`) **if it exists**. If the user has scheduled an infrastructure-provisioning task before baseline setup, run it first so that `infra/` reflects the resources that will actually be available at verification time. + +**Mandatory user confirmation before drafting rows:** +- Confirm the integration-test environment mode with the user: `real`, `mock`, `testcontainer`, or `mixed` (per dependency). Reuse the answer from the planning questionnaire when available; if missing and an interactive question tool is available, ask explicitly before generating the table. + +**Rules:** +- If the confirmed mode is `mock`: mark every dependency **mock** regardless of `infra/` presence. +- If the confirmed mode is `real`: dependency present in `infra/` (provisioned endpoint + credentials) → **real**. +- If the confirmed mode is `real` and a dependency is not present in `infra/`, mark it **mock** at the SDK / HTTP boundary and record that it is not provisioned. +- If the confirmed mode is `testcontainer`: dependency with a supported emulator/containerized dependency strategy → **testcontainer**. +- If the confirmed mode is `testcontainer` and a dependency has no viable emulator/container strategy, mark it **mock** and record `no-testcontainer-emulator` as the reason. +- If the confirmed mode is `mixed`: the mode is per dependency — ask the user for the decision per row, then apply the `real`, `testcontainer`, or `mock` rules above for each dependency individually. +- If `infra/` does not exist at all and mode is not `testcontainer`, mark every external dependency as **mock** and record `infra-missing` as the reason. +- **Cross-module service dependencies** (e.g. module A calling a service API in module B) → always **mock**. Each module's tests are self-contained; inter-module calls are mocked at the service interface boundary. +- **External services unrelated to the migration scope** (third-party APIs, internal services outside the project, legacy systems not being migrated) → always **mock**, regardless of `infra/` presence. + +**Confirm with the user before saving.** Draft the full table in chat first, then — if an interactive user-question tool is available in the current environment (e.g. `ask_user`, `vscode_askQuestions`, or any equivalent surfaced by the host) — use it to present the draft and ask the user to confirm or correct each row's `Decision` and `Auth Method`. Apply any corrections, then write the file. If no such tool is available, skip the prompt and proceed to save (do not block the workflow). + +**Output:** save to `/test-cases/infra-decision-table.md` using [infra-decision-table-template.md](infra-decision-table-template.md). One row per dependency, all columns required. The template defines the canonical column set, allowed values, decision rules, and banned phrasings. + +This file is part of the frozen baseline bundle (see Step 7) and is consumed as-is by `verify-test-baseline`. Do not proceed to Step 6 until the table is saved. + +### Step 6: Validate the Spec (Required Field Checklist) + +Before declaring the baseline frozen, validate `test-cases.md` against this checklist. Each test case MUST satisfy every item, or it is not ready to freeze. + +| # | Field | Validation rule | +|---|---|---| +| 1 | `ID` | Unique, format `TC--` (e.g. `TC-WEB-001`, `TC-WKR-002`). | +| 2 | `Category` | One of: `happy-path`, `boundary`, `special-input`, `failure`. | +| 3 | `Entry Point Type` | A short, consistent label describing the invocation mechanism (e.g. `HTTP`, `CLI`, `Scheduled`, `Message-queue listener`). The same mechanism must use the same label across all cases. | +| 4 | `Entry Point` | Exact identifier from production code: HTTP method+path, fully qualified method, CLI command, queue/topic name, service interface + method signature. No vague references. | +| 5 | `Trigger` | Concrete, technology-agnostic description of how the entry point is invoked: payload reference, headers, argv, message body. Sufficient for a code generator to construct the call. | +| 6 | `Preconditions` | All required application / resource state before the trigger, with references to `testdata/` for any seed data. Use `none` if truly none. | +| 7 | `Expected Response` | The synchronous return at the entry point: status code + body file reference for HTTP, exit code + stdout/stderr file references for CLI, return value for methods. Use `none (fire-and-forget)` for async listeners with no synchronous response. | +| 8 | `Resource Verification` | At least one bullet, OR explicit `none` with justification. Each bullet names a specific resource (object key, row PK, queue name + message shape) and the observable state. Stated in resource-neutral terms so the same check applies pre- and post-migration. | +| 9 | `Negative Verification` | For failure / skip / no-op cases, list what MUST NOT happen (e.g. "no new row in `image_metadata`"). Mandatory whenever `Category` is `failure` or behavior is "skip". | +| 10 | `Data References` | All file paths under `testdata/` cited in the case actually exist. | + +**Banned phrasings** (auto-reject): +- "should return a reasonable error" → must state exact status + body. +- "behaves as expected" / "works correctly" → must state observable outcome. +- "approximately N items" → must state exact count or a precise range with bound semantics. +- "etc." in expected outputs → enumerate completely. +- Hand-waved resource checks ("data is persisted") → must name the resource and the field-level state. + +**Entry-point coverage check**: every orchestration entry point cataloged in Step 1 appears as the `Entry Point` of at least one `happy-path` test case AND at least one `failure` test case. An entry point counts as covered if it is covered by an **existing test** (referenced as `covered-by-existing` in Step 3) OR by a **new test case** in `test-cases.md`. Both sources count toward the coverage requirement. + +Any validation failure → fix the spec. Do not freeze with defects. + +### Step 7: Freeze and Output + +1. Declare the entire `/test-cases/` folder **FROZEN**. Subsequent phases must not modify anything inside it; any required change forces a re-freeze cycle (unfreeze → amend → re-validate → re-freeze). +2. Create `${modernization-work-folder}/${taskid}/baseline-summary.md` summarizing: per-module scoping decisions, entry-point inventory, existing test inventory and extracted conventions (from Step 2), test case counts (existing-covered vs. new) by category per module, `testdata/` file list, the Step 5 infra decision table, and confirmation that the Step 6 checklist passed. +3. Commit the changes. \ No newline at end of file diff --git a/plugins/github-copilot-modernization/skills/create-test-baseline/infra-decision-table-template.md b/plugins/github-copilot-modernization/skills/create-test-baseline/infra-decision-table-template.md new file mode 100644 index 0000000..58d9a05 --- /dev/null +++ b/plugins/github-copilot-modernization/skills/create-test-baseline/infra-decision-table-template.md @@ -0,0 +1,102 @@ +# Infra Decision Table + +> This document is part of the **frozen baseline bundle**. It records, for every external dependency the migrated application talks to, whether the post-migration tests will exercise it as a **real** provisioned resource, a **testcontainer**-backed emulator/dependency, or a **mock** at the SDK / HTTP boundary. The decision is made once here and reused as-is by the `verify-test-baseline` skill. + +## Metadata + +| Field | Value | +|-------|-------| +| Project | [Application name] | +| Module | [e.g. `web`, `worker`] | +| Migration Scope | [e.g. migrate from AWS S3 + SQS to Azure Blob Storage + Service Bus] | +| Integration Test Environment | [real \| mock \| testcontainer \| mixed] | +| Created At | [YYYY-MM-DD] | +| `infra/` Snapshot | [git SHA or "infra-missing" if no infra folder] | +| Status | baseline (frozen) | + +## Decision Table + +One row per external dependency. The dependency list is derived from the target stack and the entry-point inventory in `test-cases.md` (Step 1 of `create-test-baseline`). No dependency the application talks to may be omitted. + +| Dependency | Infra Match | Decision | Auth Method | Reason | +|---|---|---|---|---| +| [Dependency name + identifier, e.g. `Azure Blob Storage (sthve4rw7qkv7k4)`] | [`Yes — ` \| `No`] | [`real` \| `testcontainer` \| `mock`] | [see allowed values below] | [Single sentence; see rules below] | + +### Required column values + +- **Dependency** — Name the concrete resource the application binds to, including its identifier when applicable (account / namespace / database / topic name). Generic categories alone (e.g. "object storage") are not acceptable. +- **Infra Match** — Exactly one of: + - `Yes — ` when a provisioned endpoint + credentials are documented in `infra/`. + - `No` when no matching resource exists in `infra/` (or `infra/` does not exist at all). +- **Decision** — Exactly one of `real`, `testcontainer`, or `mock`. No conditional values, no per-test-case overrides in this column. +- **Auth Method** — How the application authenticates to this dependency at runtime. Exactly one of: + - `managed-identity` — workload identity issued by the hosting cloud platform. + - `service-principal` — client-id with secret/certificate/federated credential. + - `username-password` — DB/basic auth user credential. + - `connection-string` — secret-bearing connection string. + - `emulator-connection-string` — local emulator/container connection string (testcontainer mode). + - `sas-token` — scoped shared access token/signature. + - `api-key` — static key or out-of-band bearer token. + - `mtls` — mutual TLS/client certificate. + - `anonymous` — no authentication. + - `n/a` — only when `Decision = mock`. + + Rules: use `managed-identity` only for deployments on identity-issuing cloud hosts; record the **post-migration** auth method only. +- **Reason** — One sentence. Must justify the Decision against the Infra Match per the rules below. + +### Testcontainer support reference + +**Has emulator — can use `testcontainer`:** + +| Dependency type | Emulator image | Dependencies | Auth method | +|---|---|---|---| +| Azure Blob / Queue / Table Storage | `mcr.microsoft.com/azure-storage/azurite` | None | `emulator-connection-string` | +| Azure Service Bus | `mcr.microsoft.com/azure-messaging/servicebus-emulator` | Companion MSSQL container + JSON config file | `emulator-connection-string` | +| Azure Event Hubs | `mcr.microsoft.com/azure-messaging/eventhubs-emulator` | Companion Azurite container + JSON config file | `emulator-connection-string` | +| Azure Cosmos DB | `mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator` | None (requires SSL trust-store setup) | `connection-string` (endpoint + emulator key) | +| Azure SQL Database | `mcr.microsoft.com/mssql/server` | None | `username-password` | +| PostgreSQL / MySQL | `postgres`, `mysql` | None | `username-password` | +| Redis / MongoDB / RabbitMQ / Kafka | standard community images | None | varies | + +> **Cosmos DB:** If SSL trust-store setup is too complex, mark `mock` with reason `no-testcontainer-emulator`. +> **Service Bus / Event Hubs:** AMQP send/receive only — management/REST APIs are not supported by the emulators. + +**No emulator — must use `mock`:** + +| Dependency type | +|---| +| Azure Key Vault | +| Azure App Configuration | +| Azure Active Directory / Entra ID | +| Azure AI / Cognitive Services | +| Third-party / external HTTP APIs | + +### Decision rules (must match Step 5 of `create-test-baseline`) + +- If `Integration Test Environment = mock`: every row's Decision MUST be `mock` regardless of `Infra Match`. +- If `Integration Test Environment = real`: `Infra Match = Yes` → Decision MUST be `real`. +- If `Integration Test Environment = real` and `Infra Match = No` while `infra/` exists: Decision MUST be `mock`. Reason should state that the dependency is not provisioned. +- If `Integration Test Environment = testcontainer`: use `testcontainer` when a viable emulator/container strategy exists for that dependency. +- If `Integration Test Environment = testcontainer` and no viable emulator/container strategy exists: Decision MUST be `mock` and Reason should include `no-testcontainer-emulator`. +- If `Integration Test Environment = mixed`: apply the `real`, `testcontainer`, or `mock` rules above per row according to the user-specified per-dependency decision. +- `infra/` does not exist at all and mode is not `testcontainer` → every row's Decision is `mock` and the Reason is `infra-missing`. Set the `infra/` Snapshot field to `infra-missing`. +- `Auth Method = n/a` is allowed **only** when `Decision = mock`. `real` and `testcontainer` rows must declare a concrete auth method. + +### Banned phrasings (auto-reject) + +- Decision values other than `real` / `testcontainer` / `mock` (e.g. `mostly real`, `real-with-fallback`, `tbd`). +- Auth Method values outside the allowed list above. Free-form descriptions (e.g. "DefaultAzureCredential", "whatever the SDK picks") are not acceptable — pick the concrete underlying credential type instead. +- Auth Method = `n/a` on a `real` or `testcontainer` row. +- Reasons that do not reference decision evidence (`infra/`, `infra-missing`, or testcontainer emulator/container evidence). +- Per-test-case carve-outs ("real for happy-path, mock for failure"). Failure-injection conflicts are handled in `verify-test-baseline` Step 4 via a re-freeze, not by splitting a row here. +- Missing or empty cells. + +## Worked Example (reference; do not copy verbatim) + +| Dependency | Infra Match | Decision | Auth Method | Reason | +|---|---|---|---|---| +| Azure Blob Storage (`sthve4rw7qkv7k4`, container `assets`) | Yes — `infra/env-config.md` | real | managed-identity | Provisioned storage account with container `assets` in resource group `rg-app-demo`. | +| Azure Service Bus (`sbemulatorns`, queue `image-processing`) | No | testcontainer | emulator-connection-string | Using Service Bus emulator container configuration for local verification; no matching provisioned namespace selected for this run. | +| Azure Service Bus (`sbhve4rw7qkv7k4`, queue `image-processing`) | Yes — `infra/env-config.md` | real | managed-identity | Provisioned namespace with queue `image-processing` in resource group `rg-app-demo`. | +| Azure Database for PostgreSQL (`pg6kt67kwkpeqji2`, database `app`) | Yes — `infra/env-config.md` | real | managed-identity | Provisioned flexible server with database `app`; migration moves from password to managed identity. | +| Third-party email API (`api.example-mail.com`) | No | mock | n/a | No provisioned credentials in `infra/`; mock at the HTTP boundary, seed from `testdata/`. | diff --git a/plugins/github-copilot-modernization/skills/create-test-baseline/test-cases-template.md b/plugins/github-copilot-modernization/skills/create-test-baseline/test-cases-template.md new file mode 100644 index 0000000..24ddd12 --- /dev/null +++ b/plugins/github-copilot-modernization/skills/create-test-baseline/test-cases-template.md @@ -0,0 +1,176 @@ +# Test Cases + +> This document is the **frozen behavioral specification** of a single module's external surface for the migration in scope. It is the source of truth that the `verify-test-baseline` skill uses to generate `*PostMigrationIT` tests against the new implementation. No test code is generated in the baseline phase. +> +> **Scoping rule:** This file MUST contain only test cases whose entry points belong to the module named below. Test cases for entry points in other modules belong in those modules' own `test-cases.md`. Modules that do not directly access any migration-relevant resource are out of scope and do not get a `test-cases.md` at all. + +## Metadata + +| Field | Value | +|-------|-------| +| Project | [Application name] | +| Module | [e.g. `web`, `worker` — the single module this file covers] | +| Migration Scope | [e.g. migrate from AWS S3 + SQS to Azure Blob Storage + Service Bus] | +| Created At | [YYYY-MM-DD] | +| Status | baseline (frozen) | +| Testing Conventions | [e.g. JUnit 5, AssertJ; naming: `should__when_`; one test class per controller] | + +## Existing Test Coverage + +List entry points already covered by existing integration tests. These are NOT duplicated as new test cases below. + +| Entry Point | Existing Test (FQ name) | Categories Covered | +|---|---|---| +| `POST /api/files/upload` | `com.example.FileControllerIT#should_upload_file_successfully` | happy-path | +| … | … | … | + +> Remove this section if no existing integration tests exist. + +## Entry-Point Inventory + +List every external boundary / orchestration entry point covered below. Cross-checked by Step 6 of the create-test-baseline skill. + +| Entry Point | Type | Source (file:symbol) | Covered by | +|---|---|---|---| +| `POST /api/files/upload` | HTTP | `web/.../FileController.java:upload` | existing: `FileControllerIT`, TC-WEB-002 | +| `processImage(ImageProcessingMessage)` on queue `image-processing` | Message-queue listener | `worker/.../ImageProcessor.java:processImage` | TC-WKR-001, TC-WKR-004 | +| … | … | … | … | + +Entry-point types: `HTTP`, `CLI`, `Scheduled`, `In-process event`, `Message-queue listener`, `Webhook`, `Streaming`, `Public method`. + +## Test Case Format + +Every test case MUST populate **all required fields below**. Use the worked example as the canonical shape. Any vague phrasing (see "Banned phrasings" in the skill) is a defect and must be fixed before freezing. + +--- + +### Worked Example — TC-WKR-001 (reference; do not copy verbatim) + +| Field | Value | +|-------|-------| +| ID | TC-WKR-001 | +| Category | happy-path | +| Entry Point Type | Message-queue listener | +| Entry Point | Listener bound to queue `image-processing`, consuming `ImageProcessingMessage` | +| Description | A valid JPEG referenced by an incoming message is downloaded, a 600px-bounded thumbnail is produced, and metadata is updated. | + +**Trigger** + +Publish one message to queue `image-processing` with body: + +```json +{ + "key": "-sample.jpg", + "contentType": "image/jpeg", + "storageType": "", + "size": 12345 +} +``` + +(`size` = byte length of `testdata/inputs/sample.jpg`.) + +**Preconditions** + +- Object with key `-sample.jpg` exists in the storage container, content byte-identical to `testdata/inputs/sample.jpg`, content-type `image/jpeg`. +- A row exists in `image_metadata` with `key = -sample.jpg`, `thumbnail_key` NULL. + +**Expected Response** + +`none (fire-and-forget)` — the listener acknowledges the message after successful processing; no synchronous response is observable. + +**Resource Verification** + +- An object with key `-sample_thumbnail.jpg` exists in the storage container with content-type `image/jpeg`. +- That thumbnail object is a readable image whose `max(width, height) <= 600`, and the aspect ratio matches the original (`testdata/inputs/sample.jpg`) within ±1 pixel. +- The row in `image_metadata` with `key = -sample.jpg` has `thumbnail_key = -sample_thumbnail.jpg` and `thumbnail_url` non-null. +- No additional rows are created in `image_metadata`. + +**Negative Verification** + +- No message is published to any downstream queue. +- No row is created in `image_metadata` with `key != -sample.jpg`. + +**Data References** + +- `testdata/inputs/sample.jpg` + +--- + +## Test Cases + +### [TC-XXX-NNN] [Operation Name] — [Category Title] + +| Field | Value | +|-------|-------| +| ID | TC-XXX-NNN | +| Category | [happy-path \| boundary \| special-input \| failure] | +| Entry Point Type | [HTTP \| CLI \| Scheduled \| In-process event \| Message-queue listener \| Webhook \| Streaming \| Public method] | +| Entry Point | [Exact identifier from production code — HTTP method+path, FQ method, CLI command, queue/topic name] | +| Description | [One sentence: what externally observable behavior this case pins down] | + +**Trigger** + +[Concrete invocation. For HTTP: method, path, headers, body (reference a `testdata/inputs/*.json` file when non-trivial). For CLI: full argv and stdin. For listener: queue/topic name and message body. Sufficient detail for a code generator to construct the call without further interpretation.] + +**Preconditions** + +- [Each prerequisite as a bullet. Reference `testdata/seed-data/*` for any seeded state. Use `none` only if literally nothing.] + +**Expected Response** + +[Synchronous return at the entry point. HTTP: status + body file reference. CLI: exit code + stdout/stderr references. Method: return value. Listener: `none (fire-and-forget)` is acceptable.] + +**Resource Verification** + +- [Each post-condition as a bullet. Name the resource (object key, table+PK, queue name + message shape) and the observable state. Resource-neutral phrasing.] +- [`none` allowed only when the operation provably touches no external resource; explain why in one phrase.] + +**Negative Verification** + +- [Required for `failure` cases and any "skip / no-op" behavior. State what MUST NOT happen, naming the resource.] +- [Omit this section ONLY for pure `happy-path` cases where no plausible side-effect-leak risk exists.] + +**Data References** + +- [Every `testdata/...` path referenced above, listed here for the freeze audit.] + +--- + +### [TC-XXX-NNN+1] … + +(Repeat for every test case. Cover all four categories per entry point.) + +--- + +## Required Field Checklist (Freeze Gate) + +Before marking this document frozen, every test case above MUST satisfy every row. Tick when validated. + +- [ ] **ID** unique, formatted `TC--`. +- [ ] **Category** is one of `happy-path`, `boundary`, `special-input`, `failure`. +- [ ] **Entry Point Type** matches one of the catalog types. +- [ ] **Entry Point** is an exact production-code identifier (no vague references). +- [ ] **Trigger** is concrete enough to construct the call mechanically (payload referenced by file, headers/argv enumerated, queue/topic named). +- [ ] **Preconditions** enumerated (or `none`); all referenced seed data exists under `testdata/`. +- [ ] **Expected Response** specifies exact status / exit code / return shape (or `none (fire-and-forget)`). +- [ ] **Resource Verification** has at least one bullet OR explicit `none` with justification; each bullet names a specific resource and the observable state. +- [ ] **Negative Verification** present for every `failure` case and every "skip / no-op" outcome. +- [ ] **Data References** complete; every path exists under `testdata/`. + +## Coverage Gate + +- [ ] Every entry point in the **Entry-Point Inventory** appears as the `Entry Point` of at least one `happy-path` case. +- [ ] Every entry point in the **Entry-Point Inventory** appears as the `Entry Point` of at least one `failure` case. +- [ ] Each entry point covers all four buckets where applicable: `happy-path`, `boundary`, `special-input`, `failure`. +- [ ] Entity examples use 2–5 representative records (no single-row, no exhaustive enumeration). + +## Banned Phrasings (Auto-Reject) + +If any of the following appears in a test case, it is a defect — fix before freezing: + +- "should return a reasonable error" → state exact status + body. +- "behaves as expected" / "works correctly" → state observable outcome. +- "approximately N items" → state exact count or precise range with bound semantics. +- "etc." / "and so on" in expected outputs → enumerate completely. +- "data is persisted" / "state is updated" without naming the resource and field-level state. +- Service-specific SDK types (`S3Object`, `BlobClient`, `SqsMessage`, …) in any field — describe in resource-neutral terms instead. diff --git a/plugins/github-copilot-modernization/skills/modernization-integration-tests/SKILL.md b/plugins/github-copilot-modernization/skills/modernization-integration-tests/SKILL.md index 6f0688c..2874f08 100644 --- a/plugins/github-copilot-modernization/skills/modernization-integration-tests/SKILL.md +++ b/plugins/github-copilot-modernization/skills/modernization-integration-tests/SKILL.md @@ -11,40 +11,38 @@ user-invocable: true disable-model-invocation: false --- -# Integration Tests for Modernized Java Applications - ## Language Support **This skill supports Java projects only.** If the source code is not Java (e.g., .NET, Python, Node.js), skip test generation and report that integration tests are not supported for this language. ## User Input - **layer** (Optional): Which layer to test (1, 2, 3, or 4). Default: 1 -- **azure-config** (Optional, Layer 3 only): Azure environment configuration -- **modernization-work-folder** (Optional): Directory path for generating plan and summary files. Default: `.github/integration-tests` +- **azure-config** (Optional, Layer 3 only): Azure environment configuration. If not provided, read from `./infra/infra-config.md` or use request tool to obtain configuration. +- **modernization-work-folder** (Optional): Directory path for generating plan and summary files. Default: `.github` - **test-root** (Optional): The root directory for integration tests. Default: current working directory. All application modules found in the directory are included in integration tests. ## Available references ### Layer 1: Local Integration Tests -**Read references/layer1-local-integration.md first**, then create TestContainers-based integration test classes. +**Read [references/layer1-local-integration.md](references/layer1-local-integration.md) first**, then create TestContainers-based integration test classes. ### Layer 2: Smoke Tests -**Read references/layer2-smoke-tests.md first.** Layer 2 uses shell-based smoke tests with docker-compose, NOT JUnit test classes. Follow the exact multi-commit workflow (artifacts → auth → restore) documented in the reference file. +**Read [references/layer2-smoke-tests.md](references/layer2-smoke-tests.md) first.** Layer 2 uses shell-based smoke tests with docker-compose, NOT JUnit test classes. Follow the exact multi-commit workflow (artifacts → auth → restore) documented in the reference file. ### Layer 3: Azure Integration Tests -**Read references/layer3-azure-integration.md first**, then create integration test classes that connect to real Azure services. +**Read [references/layer3-azure-integration.md](references/layer3-azure-integration.md) first**, then create integration test classes that connect to real Azure services. ### Layer 4: Behavioral Comparison -**Read references/layer4-behavioral-comparison.md first**, then create comparison tests that validate behavior matches between old and new implementations. +**Read [references/layer4-behavioral-comparison.md](references/layer4-behavioral-comparison.md) first**, then create comparison tests that validate behavior matches between old and new implementations. ### TestContainers Coding References -- **Azure Service Bus with TestContainers Coding Reference**, see references/azure-servicebus-testcontainers.md -- **Azure Storage with TestContainers Coding Reference**, see references/azure-storage-testcontainers.md +- **Azure Service Bus with TestContainers Coding Reference**, see [references/azure-servicebus-testcontainers.md](references/azure-servicebus-testcontainers.md) +- **Azure Storage with TestContainers Coding Reference**, see [references/azure-storage-testcontainers.md](references/azure-storage-testcontainers.md) ## Workflow 1. Analyze the project to identify modules that need to be tested and any existing integration tests. If git history is available, analyze past commits to understand which components were modified during modernization and prioritize testing those areas. -2. Create an integration test plan file at `{modernization-work-folder}/integration-test-plan.md` that outlines: +2. Create an integration test plan file at `{modernization-work-folder}/integration-tests/integration-test-plan.md` that outlines: - Testing strategy and approach for the detected app modules - Testing strategy and approach for each layer - Identified components requiring integration testing @@ -58,7 +56,7 @@ disable-model-invocation: false - Fix test code if the failure is due to unrealistic test scenarios, incorrect test setup. - Execute tests again after fixes 6. **Only proceed when all tests run and pass**, or exit after 20 attempts -7. Create an integration test summary file at `{modernization-work-folder}/integration-test-summary.md` that documents: +7. Create an integration test summary file at `{modernization-work-folder}/integration-tests/integration-test-summary.md` that documents: - All integration tests added (with file paths and descriptions) - Test coverage improvements achieved - Final test execution results @@ -67,7 +65,7 @@ disable-model-invocation: false ## Integration Tests Writing Principles **CRITICAL - Read Reference Docs First:** -- **Before starting ANY layer**, read the corresponding reference file in references/ directory +- **Before starting ANY layer**, read the corresponding reference file in [references/](./references/) directory Analyze the project if integration tests have covered all components, if not **DO ADD** new integration tests by the following principles: @@ -85,7 +83,7 @@ Analyze the project if integration tests have covered all components, if not **D - **DO NOT** add extra modules for integration tests, write integration tests in the existing modules. - **DO commit** changes separately for each layer with meaningful commit messages. Do not combine changes from different layers into a single commit. - **Layer 1, 3, 4**: Single commit per layer (e.g., `Add Layer 1 local integration tests`). Generate runner scripts and include them in the same commit. - - **Layer 2**: Multi-commit sequence as defined in references/layer2-smoke-tests.md (artifacts → auth → restore). **CRITICAL: Layer 2 does NOT create test classes - it uses shell-based smoke tests with docker-compose.** Runner scripts are part of the artifacts commit. + - **Layer 2**: Multi-commit sequence as defined in [layer2-smoke-tests.md](./references/layer2-smoke-tests.md) (artifacts → auth → restore). **CRITICAL: Layer 2 does NOT create test classes - it uses shell-based smoke tests with docker-compose.** Runner scripts are part of the artifacts commit. ### Test Isolation Convention @@ -97,13 +95,13 @@ When multiple layers coexist in the same project, tests must be distinguishable. | Layer | Class Name Suffix | Example Class Name | |-------|-------------------|--------------------| | 1 | `L1Test` | `BlobStorageL1Test`, `OrderServiceL1Test` | -| 2 | N/A - No test classes | Layer 2 uses shell-based smoke tests, not test classes. See references/layer2-smoke-tests.md | +| 2 | N/A - No test classes | Layer 2 uses shell-based smoke tests, not test classes. See [layer2-smoke-tests.md](./references/layer2-smoke-tests.md) | | 3 | `L3Test` | `AzureSqlL3Test`, `BlobStorageL3Test` | | 4 | `L4Test` | `OrderApiL4Test`, `UserServiceL4Test` | #### Tagging / Category Convention -Test classes for Layers 1, 3, 4 **MUST** be annotated with a layer-specific tag so the runner script can filter precisely. **Layer 2 does not use test classes** (see references/layer2-smoke-tests.md). +Test classes for Layers 1, 3, 4 **MUST** be annotated with a layer-specific tag so the runner script can filter precisely. **Layer 2 does not use test classes** (see [layer2-smoke-tests.md](./references/layer2-smoke-tests.md)). | Layer | JUnit 5 | JUnit 4 | |-------|---------|---------| @@ -169,7 +167,7 @@ When integration tests fail during execution, use this framework to determine wh **Business Logic Violations** - Error indicates source code violates business rules (e.g., negative inventory allowed) - Multiple similar tests fail with same pattern -**Specification Compliance** +**Specification Compliance** - Source code doesn't implement required functionality properly - Error messages show missing or incorrect behavior **Cross-Component Integration Issues** @@ -185,7 +183,7 @@ When integration tests fail during execution, use this framework to determine wh **Test Implementation Issues** - Unrealistic test data or scenarios -- Incorrect test setup (wrong mocks, invalid configurations) +- Incorrect test setup (wrong mocks, invalid configurations) - Testing implementation details rather than behavior - Race conditions or timing issues in test logic **Environmental Problems** @@ -212,12 +210,12 @@ When integration tests fail during execution, use this framework to determine wh ``` Test Failure │ - ├─ Does test model realistic business scenario? + ├─ Does test model realistic business scenario? │ ├─ No → Fix Test Code │ └─ Yes ↓ │ ├─ Does source code violate business rules? - │ ├─ Yes → Fix Source Code + │ ├─ Yes → Fix Source Code │ └─ No ↓ │ ├─ Is test setup and environment correct? @@ -257,7 +255,7 @@ After all tests are written, executed, and fixed to pass, generate a fixed runne ### Runner Script Filtering -**Layers 1, 3, 4** use tag/category filters to execute test classes. **Layer 2 uses shell commands** (see references/layer2-runner-script-templates.md). +**Layers 1, 3, 4** use tag/category filters to execute test classes. **Layer 2 uses shell commands** (see [layer2-runner-script-templates.md](./references/layer2-runner-script-templates.md)). | Layer | Maven | Gradle | |-------|-------|--------| @@ -308,7 +306,7 @@ exit $TEST_EXIT ## Completion Criteria -1. **Integration Test Plan**: Create and output a plan file at `{modernization-work-folder}/integration-test-plan.md` that includes: +1. **Integration Test Plan**: Create and output a plan file at `{modernization-work-folder}/integration-tests/integration-test-plan.md` that includes: - Analysis of existing test coverage gaps - Identified components requiring integration testing - Testing strategy and approach for each component @@ -321,10 +319,10 @@ exit $TEST_EXIT 6. **Version Control**: Commit changes separately for each layer with meaningful commit messages. Do not combine changes from different layers into a single commit. - **Layer 1, 3, 4**: Single commit per layer including test classes and runner scripts (e.g., `Add Layer 1 local integration tests`) - - **Layer 2**: Multi-commit sequence as defined in references/layer2-smoke-tests.md (minimum 3 commits: artifacts → auth → restore). Runner scripts are part of the artifacts commit. + - **Layer 2**: Multi-commit sequence as defined in [layer2-smoke-tests.md](./references/layer2-smoke-tests.md) (minimum 3 commits: artifacts → auth → restore). Runner scripts are part of the artifacts commit. - **Git ignore respect**: Use standard `git add` commands. Do not force-add files. If files in `{modernization-work-folder}` are ignored by the project's `.gitignore`, respect that. -7. **Integration Test Summary**: Create and output a summary file at `{modernization-work-folder}/integration-test-summary.md` that documents: +7. **Integration Test Summary**: Create and output a summary file at `{modernization-work-folder}/integration-tests/integration-test-summary.md` that documents: - All integration tests added (with file paths and descriptions) - Test coverage improvements achieved - Issues identified and resolved (both in source code and test code) @@ -332,13 +330,3 @@ exit $TEST_EXIT - Paths to generated runner scripts and the fixed commands to execute them - Source code changes made during testing and their purpose 8. **Runner Scripts**: Generate standardized runner scripts at `{modernization-work-folder}/integration-tests/run-layer{N}-tests.sh` and `.ps1` (see Standardized Runner Scripts section). The scripts must embed all project-specific commands so users always run the same fixed command. Include runner scripts in the layer's commit (for Layer 2, in the artifacts commit). - -**Resources:** -- references/layer1-local-integration.md -- references/layer2-smoke-tests.md -- references/layer3-azure-integration.md -- references/layer4-behavioral-comparison.md -- references/azure-auth-strategies.md -- references/azure-servicebus-testcontainers.md -- references/azure-storage-testcontainers.md -- references/layer2-runner-script-templates.md diff --git a/plugins/github-copilot-modernization/skills/team-request/SKILL.md b/plugins/github-copilot-modernization/skills/team-request/SKILL.md new file mode 100644 index 0000000..903018f --- /dev/null +++ b/plugins/github-copilot-modernization/skills/team-request/SKILL.md @@ -0,0 +1,33 @@ +--- +name: team-request +description: How team members request infrastructure connection info and handle secrets in team mode +--- + +# Team requests + +This skill is automatically loaded for all team members in team mode. It defines the requests that team members can make to each other. + +## Requesting Infrastructure Connection Info + +When your task requires connection to real Azure resources (databases, queues, storage, etc.), use the `request` tool to ask the **InfrastructureExpert** for connection values. + +**What the InfrastructureExpert provides:** +- Connection strings (e.g., for databases, message queues, storage) +- Endpoint URLs with managed identity configuration +- Confirmation of resource changes you requested + +**Workflow:** +1. Before performing work that requires real resource connections, call: + ``` + request(from: "", to: "", taskId: "", message: "I need the connection string for the PostgreSQL database") + ``` +2. The response contains ONLY the connection values — no subscription/RG/resource IDs. +3. Use the returned values to configure your code or tests. + +**Requesting resource changes:** +If you need a resource modification (e.g., create a test database, add a firewall rule, create a queue, assign a role), use the `request` tool with a message describing the change. Do NOT attempt to run `az` commands yourself for resource provisioning. + +Example: +``` +request(from: "ITTester", to: "InfraExpert", taskId: "003-integrationTest", message: "Create a test database named 'app_test' on the PostgreSQL server and return the connection string with managed identity auth") +``` diff --git a/plugins/github-copilot-modernization/skills/verify-test-baseline/SKILL.md b/plugins/github-copilot-modernization/skills/verify-test-baseline/SKILL.md new file mode 100644 index 0000000..0a587dd --- /dev/null +++ b/plugins/github-copilot-modernization/skills/verify-test-baseline/SKILL.md @@ -0,0 +1,300 @@ +--- +name: verify-test-baseline +description: Generate and run post-migration tests from the frozen baseline specification. +--- + +# Goal + +Generate executable `*PostMigrationIT` tests from the frozen baseline specification and run them against the migrated application. + +The baseline phase produces **no test code** — it produces a precise specification, including the test-cases, infra-decision-table, and testdata. This skill is the sole place where integration test code is generated. + +When the baseline decision table includes `testcontainer` rows, verification MUST run those dependencies against containerized emulators/services (not cloud resources and not mocks). + +## User Input + +- **taskid** — Identifier for this verification run. +- **modernization-work-folder** — Folder under which the verification summary and decision table are written. + +## Terminology + +**Event-sourced subscriber** — any entry point invoked by an external event source rather than by a synchronous caller: message-queue listeners, event-bus / event-hub / event-grid handlers, storage-event triggers (e.g. blob-created or object-deleted handlers), DB change-feed / CDC handlers, inbound-email handlers, file-system watchers. Throughout this document, "event-sourced subscriber" refers to this entire family; the only legitimate trigger for such an entry point is a real event produced on the declared source via its SDK or wire protocol. + +**Testcontainer dependency** — an external dependency marked `testcontainer` in `infra-decision-table.md`; verification must instantiate and use a containerized emulator/service for that dependency via the project's Testcontainers stack. + +## TestContainer References + +When any dependency is marked `testcontainer`, read and apply these references before writing tests: + +- [azure-auth-strategies.md](../modernization-integration-tests/references/azure-auth-strategies.md) +- [azure-servicebus-testcontainers.md](../modernization-integration-tests/references/azure-servicebus-testcontainers.md) +- [azure-storage-testcontainers.md](../modernization-integration-tests/references/azure-storage-testcontainers.md) + +## Principles + +- The spec is the source of truth. If a test case cannot be generated from its fields as-written, the defect is in the spec — coordinate a re-freeze (Step 8), do not invent details in the generated test. +- Post-migration tests are **additions**, never replacements. +- **Strict 1:1 mapping.** Exactly one test method per `TC-*` in `test-cases.md`. No extra tests, no missing tests. Test method count == TC count. **Banned extras include:** infrastructure connectivity tests ("can connect to DB", "can create SDK client"), SDK sanity tests ("can send/receive message", "can read secret"), backing-store CRUD tests that have no entry point in the inventory, and authentication/credential validation tests. If it is not a `TC-*`, it must not exist. +- **TC-ID traceability.** Every test method MUST include its `TC-*` ID in the method name itself (e.g. `tcEjb001_collectLocationHappyPath`, `tc_ejb_001_collect_location_happy_path`). A comment or display name alone is insufficient — the method name is the primary index for auditing coverage. +- **Trigger fidelity over convenience.** The trigger is the contract. Any test whose trigger is not the declared `Entry Point` — for example, one that invokes a cloud SDK, a repository, an internal service, or a private helper instead — is invalid and must be regenerated, even if it passes. If the spec declares an application method as the entry point, the test MUST call that method through its public interface — not re-implement the logic inline using lower-level APIs (e.g. direct DB writes, manual computations). If the spec declares a message-queue listener as the entry point, the test MUST send a message to the queue — not call the listener's handler or an internal service directly. +- **Assertion completeness.** A test must assert every bullet under `Expected Response`, `Resource Verification`, and `Negative Verification`. Existence-only or no-throw-only checks (asserting only that a resource exists, that a call did not throw, or that a value is non-null) are insufficient on their own. +- **No production-bug workarounds.** If a test fails because the migrated code is wrong, hand the work back to the migration engineer per Step 7. Do not patch the test to bypass the broken entry point, and do not loosen / rewrite assertions to accept the buggy response. "Documenting the current behavior" by changing expected response codes, status, or payload to match what the broken code returns is a workaround — not a fix. +- **No hardcoded environment topology.** Generated tests must not hardcode environment-specific resource identifiers (account names, namespaces, queue/topic names, hostnames, connection strings, tenant/subscription IDs, database hosts, secrets). Resolve these from test-only configuration wired to `infra/` outputs and environment variables. +- **No Azure resource writes.** This skill MUST NOT create, update, delete, or otherwise modify Azure resources. Any `az` CLI command that performs a write operation (e.g. `az role assignment create`, `az storage account create`, `az keyvault set-policy`, `az group create`, `az resource update`, `az ad app create`) is forbidden. Read-only `az` commands (e.g. `az account show`, `az role assignment list`, `az resource list`) are permitted for diagnostic purposes only. If a missing role assignment, resource, or configuration is identified, hand over to the Infra Expert per Step 7 — do not provision or reconfigure Azure resources from within this skill. + +## Layout + +Inputs (frozen, produced by the setupBaseline task) live under each in-scope module's **test source root** — the directory the build tool already uses for tests (e.g. `src/test/` for Maven/Gradle Java, `tests/` for Python / Node / Go, `/test/` for multi-module repos). In a multi-module project, each module that was determined to be in-scope during the baseline phase has its own independent `test-cases/` folder. Modules that were marked out-of-scope (no migration-relevant resource access) will not have a `test-cases/` folder — skip them. + +``` +/ +└── test-cases/ # FROZEN folder — created in Phase 1 + ├── test-cases.md # FROZEN — the behavior spec (scoped to this module only) + ├── infra-decision-table.md # FROZEN — mock/real/testcontainer decision per external dependency + └── testdata/ # FROZEN — fixtures referenced by test-cases.md +``` + +Outputs of this skill: + +- `${modernization-work-folder}/${taskid}/post-migration-plan.md` — the TC→test planning table written in Step 4 (created/overwritten on each run). +- `*PostMigrationIT` source files — follow the project's existing test layout conventions. +- `${modernization-work-folder}/${taskid}/verification-summary.md` — final report (Step 9). + +Reused inputs from the baseline phase: + +- `/test-cases/infra-decision-table.md` — mock/real/testcontainer decision per external dependency, produced by `create-test-baseline`. This skill consumes it as-is. + +## Workflow + +### Step 1 — Verify Baseline Integrity + +For each in-scope module, locate `/test-cases/` and confirm `test-cases.md`, `infra-decision-table.md`, and `testdata/` are byte-identical to the baseline commit. Any drift → request revert. If `test-cases.md` is missing entirely for a module that was marked in-scope, abort: the setupBaseline task was supposed to run first — surface this as a plan-ordering bug, do not proceed. Modules without a `test-cases/` folder were determined out-of-scope during the baseline phase (no migration-relevant resource access) — skip them. + +### Step 2 — Load Infra Decision Table from Baseline (mandatory gate) + +The mock/real/testcontainer decision for every external dependency is made in the baseline phase, not here. Verification reuses it as-is. + +1. Load `/test-cases/infra-decision-table.md` produced by `create-test-baseline`. If it is missing, abort and surface this as a plan-ordering bug — do not regenerate it here. +2. Sanity-check the table against runtime prerequisites: + - every row marked **real** must still have a matching provisioned resource in `infra/`. + - every row marked **testcontainer** must still have a viable emulator/container strategy (image, config, dependency containers, and test framework support) in the repo and test runtime. + If prerequisites drift (resource removed, endpoint changed, credential type changed, missing emulator config, unsupported testcontainers version), surface the drift → Step 8 (re-freeze cycle); do not silently downgrade decision modes here. +3. Treat the loaded table as the source of truth for all subsequent steps. + +Do not proceed until the table is loaded and the sanity check passes. + +### Step 3 — Validate Spec Readiness + +Before generating code, audit `test-cases.md` against the **Required Field Checklist** defined in Step 5 of the `create-test-baseline` skill. + +- Every test case has all required fields populated (ID, Category, Entry Point Type, Entry Point, Trigger, Preconditions, Expected Response, Resource Verification, Negative Verification where required, Data References). +- No banned phrasings remain. +- Every `testdata/...` path referenced exists. +- The Entry-Point Inventory matches what is exercised by the cases. + +Any defect → Step 8 (re-freeze cycle). Do not paper over spec defects in generated code. + +### Step 4 — Plan Post-Migration Tests + +Inputs: `test-cases.md`, the infra decision table loaded in Step 2. + +1. **Mirror the spec, exactly once.** For each `TC-*` in `test-cases.md`, plan exactly one test method. No extra tests. No collapsing two TCs into one. No splitting one TC across multiple test methods (sub-steps go inside the single method body). +2. **Map each entry point to its concrete trigger mechanism on the new stack.** The `Trigger` field is technology-agnostic; resolve it to the actual mechanism that exists in the migrated codebase. Always trigger via the same outside-in path the entry point is invoked from in production. Pick the most realistic public driver the test framework offers: + - HTTP / network handlers → framework's HTTP test client. + - CLI commands → CLI runner / process invocation. + - Library APIs → call the published API directly. + - EJB entry points (remote/local business interfaces) → invoke through the container-managed EJB interface/proxy used by external callers; do not call bean implementation classes directly. + - **Event-sourced subscribers** (see Terminology) → produce a real event on the declared source via its SDK or wire protocol (publish a message to the queue/topic, upload/delete the blob, insert/update the watched DB row, send an SMTP message, write the watched file). The application is the subscriber; the only realistic trigger is a real event on the source it subscribes to. + - Scheduled jobs / cron → framework's "run now" hook (e.g. scheduler `triggerJob`, manual invocation of the scheduled-task dispatcher). This is framework-driven, not SDK-driven. + + When in doubt, prefer the mechanism a real external caller or event source would use over a test-only shortcut. +3. **Reject in-spec but infeasible decision-mode cases early.** If a test case requires failure injection on a dependency marked **real** or **testcontainer** (e.g. "storage unavailable", "backend throws IOException", "corrupt object content") and there is no way to trigger that condition from the declared entry point under that mode, do not silently skip and do not silently switch modes — surface the conflict → Step 8 (re-freeze) so the infra-decision-table is amended or the case is reformulated. +4. **Close entry-point coverage gaps.** Scan production code for orchestration entry points. For each one not present in the Entry-Point Inventory of `test-cases.md`, surface the gap → Step 8 (re-freeze) so the spec is updated first. Do not silently add post-migration-only cases. +5. **Produce a planning table (mandatory artifact).** Before writing any code, emit the TC→test mapping table and **save it to `${modernization-work-folder}/${taskid}/post-migration-plan.md`** (create or overwrite). The file MUST contain one row per `TC-*`; rows whose `Trigger Mechanism` is anything other than the declared entry point's outside-in driver, or whose `Fixtures Loaded` is empty while `Data References` is non-empty, must be revised before proceeding. The same table is later linked from the Step 9 verification summary. + + | TC ID | Test Location (class/file → method) | Declared Entry Point | Trigger Mechanism (concrete) | Fixtures Loaded (from `Data References`) | Non-mock Deps Touched (`real`/`testcontainer`) | Config Source | Cleanup Path | + |---|---|---|---|---|---|---|---| + | TC-XXX-001 | _e.g._ `FooPostMigrationIT.uploadHappyPath` | _e.g._ `POST /foo/upload` | _e.g._ HTTP test client multipart POST to `/foo/upload` | `testdata/inputs/sample.jpg`, `testdata/expectations/upload-success.json` | _e.g._ Blob (`testcontainer`), Queue (`real`) | _e.g._ `application-integrationtest.yml` + env vars mapped from `infra/` outputs and Testcontainers runtime properties | _e.g._ `POST /foo/delete/{key}` | + + The row above is illustrative; use the test-class / method / driver naming conventions of the migrated project's language and framework. + `Config Source` is mandatory and must name where each non-mock dependency endpoint/identifier comes from. Any row that implies inline literals in test code must be revised before generation. + +#### Forbidden trigger patterns (rejected by the Step 5 pre-generation audit) + +- HTTP entry point in spec, but the test calls a cloud / storage / messaging SDK directly as the trigger _(e.g. invoking a blob client's upload method, a queue sender client's send method, an object-store put-object call)_. +- HTTP entry point in spec, but the test calls an internal application service, handler, repository, or sender component directly as the trigger. +- EJB entry point in spec, but the test calls the bean implementation class directly (or reflection-invokes its methods) instead of invoking through the container-managed EJB interface/proxy. +- Event-sourced subscriber entry point in spec (see Terminology), but the test calls the subscriber's internal handler method directly _(e.g. invoking the processing service method that the listener delegates to, or feeding a hand-built mocked message/event context into the handler, including via reflection on a private method)_ instead of producing a real event on the declared source. +- Scheduled job entry point in spec, but the test calls the job's run method directly instead of using the framework's scheduled-task invocation hook. +- Backing store (DB, cache, blob container, search index, etc.) used as the *trigger* of a test when no entry point in the inventory exposes that store. Backing stores are not entry points; they may only appear in **Preconditions / Resource Verification**, never as the trigger. +- Any private helper in the test that re-implements production parsing or key-derivation logic _(e.g. a local copy of an "extract original key from thumbnail key" routine)_ — assert against the spec's declared post-state, not against a re-derived expectation. +- Any test helper that re-implements the entry point's **business logic** _(e.g. manually writing to a DB and computing a classification value inline, instead of calling the application method that does both)_. The test must call the actual entry point and verify its output — not simulate what the entry point would do. +- Any test class that tests SDK/infrastructure capabilities (DB connectivity, message broker send/receive, secret retrieval, credential validation) without mapping to a `TC-*` in the spec. These are not post-migration integration tests. + +### Step 5 — Generate Post-Migration Tests + +#### Pre-generation trigger audit (mandatory gate) + +Before writing any test code, emit a **trigger-line pseudocode table** for every planned test method. For each row, write the single line of code (or pseudocode) that will serve as the trigger, then self-check it against the Forbidden trigger patterns in Step 4. The table format: + +| TC ID | Trigger pseudocode | Entry Point Type (from spec) | Violates Step 4 forbidden patterns? | +|---|---|---|---| +| TC-XXX-001 | `serviceBusSender.sendMessage(queue, messageBody)` | Message-queue listener | No — publishes real event to declared source | + +Any row whose last column is anything other than `No` MUST be revised until it passes. Do not proceed to code generation with any unresolved row. + +#### Trigger rules + +- **Trigger only via the declared entry point.** The constraint applies to the **trigger** of the test, not to setup/teardown. See the Forbidden trigger patterns in Step 4 for the concrete anti-patterns that must be rejected. +- **Seeding preconditions and resource verification may use SDKs directly.** When a test case's `Preconditions` or `Resource Verification` requires state on a real or testcontainer-backed resource (object in a container, row in a table, message on a queue) and the application exposes no public entry point to create or read that state, the test setup / verification step MAY call the resource's SDK directly. Seed data comes from `testdata/`. This is setup/observation, not the trigger. +- **Async event-sourced tests** must wait on the **observable post-condition** using the language/framework's idiomatic async-wait helper that polls until the condition holds or a timeout elapses _(e.g. an `Awaitility`-style polling helper in JVM languages, `WaitFor` / polling loops in .NET, `pytest`-style retry helpers in Python)_. Never use a fixed-duration sleep, and never assert immediately after emitting the event. + +#### Assertion rules + +- **Every bullet in the spec is an assertion.** Walk `Expected Response`, `Resource Verification`, and `Negative Verification` bullet-by-bullet. Each bullet maps to at least one assertion in the test body. Missing any bullet → regenerate. +- **Load every fixture in `Data References`.** Each path under `Data References` must be loaded by the test through the language's normal resource-loading mechanism _(e.g. classpath resource stream in JVM, embedded resource / file read in .NET, file open in Python/Node/Go)_ and used either as input or as the expected value for an assertion. Unused fixtures are a planning bug — either the test under-asserts, or the spec lists a fixture it doesn't need (→ Step 8). +- **No existence-only / no-throw-only tests.** Assertions that only check resource existence, only check that a call did not throw, or only check non-null do not satisfy `Resource Verification`. Assert content, fields, sizes, statuses, redirect targets, message bodies, and DB column values exactly as the spec states. +- **Negative verification is mandatory where the spec lists it.** For every bullet under `Negative Verification` _(e.g. "no new row inserted", "no message published", "no thumbnail created")_, the test must perform the observation that proves the negative — not just skip it. + +#### Test isolation rules + +- **One TC = one independent test method.** No shared mutable state across test methods in the same class/module. No ordered-execution chains where one method's success is required for the next _(e.g. JUnit `@Order`, NUnit `[Order]`, xUnit `IClassFixture` for ordering, pytest fixture ordering tricks)_. No skip-when-previous-test-passed coupling _(e.g. `Assumptions.assumeTrue(previousState != null)`, `Skip.If(...)`)_. Every test sets up its own preconditions and tears them down. +- **Random keys per test run.** Use a fresh unique suffix (GUID / random string / timestamp+nonce) for every created entity; never deterministic names, since real resources are shared across parallel runs and re-runs. + +#### Stack & dependency rules + +- **Boot the full application stack** — no sliced/partial test contexts when any dependency is "real" or `testcontainer`. +- **Real dependencies stay real.** Do not stub, fake, or mock anything marked "real" in the decision table at any layer. +- **Testcontainer dependencies stay testcontainer-backed.** Do not replace `testcontainer` rows with mocks or cloud resources; instantiate and wire the required emulator/service containers. +- **Mocked dependencies** (only those marked "mock"): mock at SDK / HTTP boundary, seed from `testdata/`, assert on outbound requests as well as return values. +- **Cross-module service dependencies** (marked "mock" in the decision table): mock at the service interface boundary so each module's tests are self-contained. The test verifies the module's own behavior given controlled responses from the mocked cross-module dependency. Do not let cross-module calls fall through to a real sibling module. +- **Non-migration-scope external services** (marked "mock" in the decision table): mock at the SDK / HTTP boundary. These include third-party APIs, internal services outside the project, and legacy systems not being migrated. +- **Test-only configuration** points to: + - real endpoints from `infra/` for `real` rows, and + - dynamic container endpoints/connection strings for `testcontainer` rows, + via the project's standard mechanism (Spring profile, `.env`, `appsettings.IntegrationTest.json`, env vars). Activated for these tests only; do not modify production config. + +#### Auth rules + +- **Auth requirements depend on the infra decision table.** + - `real` rows: cloud credentials are required. + - `testcontainer` rows: cloud credentials are NOT required; use emulator/container credentials or anonymous/local auth. + - `mock` rows: no cloud credentials required. +- **Pre-flight auth check** in test setup, applied per real dependency the test touches. + - **Local runs (non-Azure host).** Managed Identity is unavailable — the test MUST authenticate as the developer's `az login` principal (e.g. via `DefaultAzureCredential` / `AzureCliCredential`). Do not fabricate a managed-identity client ID, do not point at IMDS, and do not require a service-principal secret for local runs. + - **CI / Azure-hosted runs.** Use the configured Managed Identity when available, otherwise the CI service-principal env vars (`AZURE_CLIENT_ID` / `AZURE_TENANT_ID` / `AZURE_CLIENT_SECRET` or federated credentials). + - **The pre-flight check is a fail-fast gate, not a silent-skip.** If credentials for a real dependency are missing or insufficient, the test MUST fail loudly (assertion failure / explicit error) so Step 7 picks it up as an **Infra issue** and escalates. Do NOT implement "gracefully abort / mark as skipped / return early" patterns _(e.g. `Assumptions.assumeTrue(credentialsAvailable)`, `Skip.If(...)`, JUnit `@EnabledIfEnvironmentVariable`, early `return` in `@BeforeAll`, try/catch that swallows the auth exception)_ — those let the verification phase complete with zero real assertions executed against the migrated stack. + - **All-mock and all-testcontainer tests do not skip on missing cloud credentials.** If a test's dependencies include no `real` rows, it must run unconditionally; a missing `az login` is not a valid reason to skip. + +#### Cleanup rules + +- **Cleanup via entry points first.** Prefer the application's own delete entry point for cleanup so test credentials need no extra data-plane permissions. Fall back to SDK cleanup only when no delete entry point exists. Run cleanup in `finally` / teardown; ignore "not found". Never use SDK cleanup as a workaround for a broken application delete path — if the application's delete entry point fails, that is a production bug (Step 7), not a cleanup-strategy choice. For mocked dependencies, cleanup is reset of in-memory state — still required so tests stay independent. + +### Step 6 — Validate and Run + +Before running, run the **pre-run validation checklist** against generated code. Any `No` → regenerate (Step 5) or escalate to Step 8. Do not proceed to execution with a failing checklist. + +**Coverage & mapping** + +- [ ] Test method count equals `TC-*` count in `test-cases.md` (no extras, no missing). **Zero test classes may exist that are not mapped to at least one TC-*.** Infrastructure-only, SDK-sanity, or connectivity test classes are forbidden. +- [ ] Every `TC-*` is referenced by exactly one test method whose **method name contains the TC-ID** (e.g. `tcEjb001_...`). A comment or `@DisplayName` alone is insufficient. +- [ ] No test class / module exists for an entry point absent from the Entry-Point Inventory (no DB-only / cache-only / SDK-only test class when those are backing stores rather than entry points). +- [ ] `${modernization-work-folder}/${taskid}/post-migration-plan.md` exists and has one row per `TC-*`. + +**Trigger correctness (per test)** + +- [ ] The line that invokes the system under test matches the declared `Entry Point` and does not match any pattern in Step 4 "Forbidden trigger patterns". +- [ ] For event-sourced subscriber entry points (see Terminology): the trigger produces a real event on the declared source via its SDK or wire protocol; an async-wait helper polls for the post-condition; no direct call to the subscriber's handler method (including via reflection) and no hand-fabricated message/event context. + +**Assertion completeness (per test)** + +- [ ] Every bullet under `Expected Response` is asserted. +- [ ] Every bullet under `Resource Verification` is asserted. +- [ ] Every bullet under `Negative Verification` is asserted (where the spec lists it). +- [ ] Every path in `Data References` is loaded by the test. +- [ ] No test relies solely on existence / non-null / no-throw assertions. + +**Isolation** + +- [ ] No shared mutable state across test methods. +- [ ] No ordered-execution chains where later tests depend on earlier tests succeeding. +- [ ] All created entities use random suffixes. + +**Infra alignment** + +- [ ] No mocks/stubs/fakes for any "real" or `testcontainer` dependency. +- [ ] Full application stack boots when any dependency is "real" or `testcontainer`. +- [ ] Test-only configuration exists and points at real endpoints in `infra/` for `real` rows and container endpoints for `testcontainer` rows. +- [ ] No hardcoded environment-specific topology in generated test source; all dependency identifiers/endpoints are supplied via test-only config and/or env vars. +- [ ] No generated "global cleanup" or "queue drain" step that alters shared resources beyond the TC-scoped setup/cleanup required by the spec. +- [ ] No test case requires failure injection (e.g. "storage unavailable", "backend throws IOException") on a dependency marked **real** or `testcontainer` without the ability to trigger that condition from the declared entry point. Any such case should have been surfaced in Step 4.3 and sent to Step 8 (re-freeze). + +Run all `*PostMigrationIT` tests. Required: **100% pass**, and the run MUST be a real execution that reached the application. For each TC, dependencies must follow the decision table exactly: `real` rows hit real provisioned resources, `testcontainer` rows hit instantiated containers/emulators, and `mock` rows are served by configured test doubles. + +**Execution is mandatory — compile/unit-test success is not verification.** + +- Invoke the project's integration-test phase explicitly _(e.g. Maven `mvn verify` / `mvn failsafe:integration-test`, Gradle `./gradlew integrationTest`, `dotnet test` against the IT project, `pytest tests/integration`, `go test -tags=integration ./...`)_. Confirm from the runner's output that each `*PostMigrationIT` method was actually executed (e.g. Failsafe `Tests run: N` ≥ TC count, not `Tests run: 0`). A green build that ran 0 IT methods is **not** a pass. +- The following do **not** count as runtime verification and MUST NOT be used as justification to mark the task `success`: + - `mvn compile` / `mvn test-compile` / type-check only. + - The unit-test phase (`mvn test`, `dotnet test` without the IT project, `pytest` without the integration marker) — by convention `*IT` / integration tests are excluded from this phase. + - Static review of the generated test files ("no `@MockBean` present", "assertions look right") without running them. +- **Blocker routing depends on dependency mode.** + - Real-dependency blockers (no credentials, no network reachability, missing role assignment, infra not provisioned) are Infra issues per Step 7 and go to the Infra Expert. + - Testcontainer blockers (Docker unavailable, image pull denied, emulator dependency container missing, unsupported testcontainers version) are test setup issues, must be reported as runtime errors, and do **not** go to the Infra Expert. + These blockers are **not** a valid reason to skip TCs whose dependencies are all **mock**: those tests must still execute and pass before the task can be `success`. +- **Accidental-real-call guard for mock TCs.** A test whose decision-table row is `mock` but which silently falls back to a real network call (because the mock wasn't wired, or because the SDK uses default credentials when no stub is present) is a **test bug**, not a pass — fix the mock wiring; do not declare success on the strength of an unintended real call. +- **Accidental-mode-mismatch guard for testcontainer TCs.** A test whose decision-table row is `testcontainer` but which silently falls back to a real cloud call or a mock is a **test bug**, not a pass — fix container wiring. + +**Task status outcome (mandatory mapping).** Pick exactly one based on the actual runtime result — never on "compile passed" or "unit tests passed". Runtime coverage checks apply to TCs whose decision-table rows are **real** and/or **testcontainer**: + +| Situation | Status | +|---|---| +| Every `*PostMigrationIT` ran AND 100% pass; real-dep TCs hit real resources, testcontainer-dep TCs hit configured containers/emulators, and mock-dep TCs were served by the configured doubles | `success` | +| Verification is paused waiting on a hand-off (Infra agent, migration developer for production bug, re-freeze cycle); will resume | `pending` (with summary of who/what is blocking) | +| `*PostMigrationIT` executed and at least one failed, AND the cause is a production bug or a test bug that cannot be reconciled with the spec, AND no further hand-off is in progress | `failed` | +| Real-dep TCs could not be started or completed and the blocker cannot be resolved (Infra Expert unavailable/declined, or infra drift cannot be fixed in this run), OR testcontainer-dep TCs could not be started or completed and required container runtime/emulator setup cannot be restored in this run | `failed` | +| Mock-dep TCs were not executed for any reason, OR verification produced no real execution evidence at all (zero `*PostMigrationIT` methods executed, runner reported `Tests run: 0`, or only compile/unit phases ran) | `failed` | + +The status `success` is permitted only when the Runtime Execution Evidence required by Step 9 can be filled in with real numbers for every TC. If you would have to write "integration tests were not executed because..." for any TC in the summary, the status is **not** `success` — it is `pending` (real-dep infra handoff active, or testcontainer setup still unresolved) or `failed` (handoff exhausted, mock-dep TCs unrun, or no path forward). + +### Step 7 — Classify and Route Failures + +This section is the single reference point for failure classification. Steps 5 and 6 route here when a runtime or generation-time failure occurs. + +The spec is the source of truth. Before deciding it is a test bug, prove the test contradicts the spec. The default classification of any disagreement between spec and runtime behavior is **production bug**. + +- **Test bug** (the generated test does not faithfully implement what the spec says) → fix the test. Examples: wrong fixture loaded, wrong assertion value relative to the spec, wrong trigger mechanism, missing async wait. The signal: the spec says X, the test asserts Y, the runtime returns X. +- **Production bug** (the runtime contradicts the spec) → **stop, do not patch the test.** Hand back to the migration developer (see handover protocol below). +- **Spec gap** (case requires failure injection that real infra cannot produce, fixture missing, entry point unlisted) → Step 8 (re-freeze). Do not silently downgrade a "real" dependency to a mock to make a test pass. +- **Infra issue (real dependencies only)** — auth/configuration/network/endpoint problem on a real resource (e.g. missing role assignment, endpoint unreachable, infra not provisioned) → hand over to the Infra Expert (see handover protocol below). Do not attempt infra changes from inside the test skill. +- **Testcontainer setup issue** — container-runtime/emulator problem for a `testcontainer` dependency (e.g. Docker daemon unavailable, image pull failure, emulator config missing, unsupported testcontainers version) → report as an error, fix in this skill; if unresolved, use `ask_user`. Do not route these to the Infra Expert. + +**Handover protocol (production bug / infra issue):** + +Hand over per the teams SOP. The hand-back message must include: + +| Classification | Recipient | Message must include | +|---|---|---| +| Production bug | Migration developer | Which `TC-*` failed, the declared expected behavior from the spec, the observed behavior from the run, and the suspected production cause | +| Infra issue (real dependencies only) | Infra Expert | Which `*PostMigrationIT` could not run, the exact error or missing prerequisite, and the real resource(s) involved (resource group/account/namespace/server names) | + +**Prohibited responses (both):** do not patch the test to work around the problem — no trigger rewrites, no loosened assertions, no auth-setup changes beyond Step 5. Do not loop indefinitely or flip to `success`. + +**Exhausted hand-off:** if the recipient is unavailable or does not exist in `teams-roles.json`, fall back to `ask_user`. If `ask_user` is also unavailable or the user declines to act → close the task as `failed` per the status mapping in Step 6. + +### Step 8 — Re-Freeze Cycle for Spec Defects + +Whenever any of the following occurs — the Step 3 audit fails; Step 4 finds an uncovered entry point, an infeasible-as-real failure-injection case, or a planning gap that cannot be expressed against the current spec; Step 6 finds a generated test cannot be reconciled with the spec; or any new fixture / scenario is needed — **coordinate a baseline re-freeze**: unfreeze the contents of `test-cases/` → amend (`test-cases.md`, `testdata/`, and/or `infra-decision-table.md`) → re-run the create-test-baseline freeze gate → re-freeze. There is no side channel for adding fixtures or cases in Phase 3. + +### Step 9 — Report + +Create `${modernization-work-folder}/${taskid}/verification-summary.md` summarizing decisions, results, and gaps. This is the only **report** artifact — do not emit additional report / status markdown files. (The planning table from Step 4 and the `*PostMigrationIT` source files from Step 5 are separate required outputs and continue to exist.) + +The summary MUST include a **Runtime Execution Evidence** section with: + +- The exact command(s) used to run the integration tests (e.g. `mvn -pl web,worker verify`). +- The runner's tests-run / failures / errors / skipped counts per module, copied from the actual output. +- A one-line confirmation that the count of executed `*PostMigrationIT` methods equals the count of `TC-*` planned in Step 4. +- A per-TC breakdown of **real vs testcontainer vs mock dependencies actually used** at runtime, matched against the Step 2 decision table. For real-dep TCs: the authentication mode used (`az login` principal name, MI client ID, or CI SP) and the target resource identifiers (resource group + account/namespace/server) that were actually contacted. For testcontainer-dep TCs: container image(s), mapped endpoints, and emulator auth strategy used. For mock-dep TCs: the test-double mechanism used (e.g. `@MockBean`, WireMock stub, in-memory fake) so it is auditable that no accidental real call occurred. + +If this section cannot be populated with real execution data for every TC (real-dep TCs with real-resource evidence, testcontainer-dep TCs with container runtime evidence, mock-dep TCs with mock-wiring evidence), the task is not eligible to be marked `success` — go back to Step 6 and resolve the real-dependency Infra issue or the testcontainer setup issue (per Step 7), or fix the missing execution first.