diff --git a/.github/prompts/address-pr-comments.prompt.md b/.github/prompts/address-pr-comments.prompt.md new file mode 100644 index 000000000..a218dcfc1 --- /dev/null +++ b/.github/prompts/address-pr-comments.prompt.md @@ -0,0 +1,370 @@ +--- +agent: agent +name: address-pr-comments +description: Review and address all unresolved comments on a GitHub Pull Request, implement the requested changes, and commit the fixes. +model: GPT-5.4 (copilot) +--- + +# Address PR Comments Agent + +## Purpose + +This prompt guides an AI agent to review and address all **unresolved** comments on a GitHub Pull Request, +implement the requested changes, and commit the fixes. When replying to review feedback, +prefer a **single batched review submission** so reviewers receive one grouped notification instead +of one notification per reply. + +## Prerequisites + +- GitHub CLI (`gh`) installed and authenticated +- GitHub MCP server available with PR tools activated +- Current branch matches the PR branch being addressed +- Repository has uncommitted changes handled (stash or commit first) +- Ensure `GH_PAGER` is set to `cat` to avoid pagination issues with less requiring user interaction + +## User Input + +```text +$ARGUMENTS +``` + +The user may provide: + +- A PR number (e.g., `26`) +- A PR URL (e.g., `https://github.com/owner/repo/pull/26`) +- Nothing (use current branch's PR) + +## Execution Flow + +### Phase 1: PR Discovery & Context Gathering + +1. **Determine the target PR**: + - If PR number provided in `$ARGUMENTS`, use it directly + - If PR URL provided, extract the PR number + - If no argument, detect PR from current branch: + + ```bash + gh pr view --json number --jq '.number' + ``` + +2. **Verify branch alignment**: + - Get current git branch: `git branch --show-current` + - Get PR head branch via `gh pr view --json headRefName --jq '.headRefName'` + - If branches don't match, STOP and ask user to switch branches first + +3. **Fetch PR metadata**: + + ```bash + gh pr view --json title,body,state,reviewDecision,reviews,comments + ``` + +### Phase 2: Retrieve All Review Comments + +1. **Get repository details**: + + ```bash + gh repo view --json owner,name --jq '{owner: .owner.login, name: .name}' + ``` + +2. **Get all PR review threads with resolution status**: + + ```bash + # Replace OWNER, REPO, and PR_NUMBER with actual values + gh api graphql -f query=' + query($owner: String!, $repo: String!, $pr: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $pr) { + reviewThreads(first: 100) { + nodes { + id + isResolved + isOutdated + path + line + comments(first: 10) { + nodes { + databaseId + body + author { login } + createdAt + } + } + } + } + } + } + } + ' -f owner=OWNER -f repo=REPO -F pr=PR_NUMBER + ``` + +3. **Filter to unresolved threads only**: + - `isResolved: false` + - Optionally include `isOutdated: false` to skip comments on old code + +### Phase 3: Analyze & Categorize Comments + +For each unresolved comment, categorize as: + +| Category | Action Required | +| ----------------- | ---------------------------------------- | +| **Code Change** | Modify source file at specified location | +| **Documentation** | Update docs, comments, or Javadoc | +| **Test Addition** | Add or modify test cases | +| **Clarification** | Reply with explanation (no code change) | +| **Out of Scope** | Mark for follow-up issue creation | +| **Disagree** | Prepare response explaining rationale | + +Create a structured todo list: + +```json +{ + "pr_number": 26, + "unresolved_count": 5, + "comments": [ + { + "id": "thread_id", + "path": "java/pom.xml", + "line": 42, + "category": "Code Change", + "summary": "Add error handling for edge case", + "reviewer": "reviewer_username", + "action_plan": "Add match arm for empty input" + } + ] +} +``` + +### Phase 4: Address Each Comment + +For each comment requiring code changes: + +1. **Read the relevant file context**: + - Use `read_file` tool to get surrounding context (±20 lines around the comment line) + - Understand the current implementation + +2. **Implement the fix**: + - Use `replace_string_in_file` or `multi_replace_string_in_file` for edits + - Follow Constitution principles (TDD, Clean Code, Security-First) + - If the fix requires new tests, add them first (Red-Green-Refactor) + +3. **Validate the change**: + - Run `./mvnw fmt:format` in each modified Maven module (for example `java/` and `api-tests/`) to keep formatting checks green + - Run relevant module tests such as `./mvnw test` or `./mvnw verify` in `java/` and `api-tests/` + - If the change touches the Azure coverage tooling, run `npm ci` and the relevant validation command in `build/azDevOps/azure/coverage/` + +4. **Prepare reply text** for each addressed comment: + + ```markdown + Addressed in commit [SHA]: + + - [Brief description of the change] + - [Any additional context or decisions made] + ``` + +5. **Prepare batched review content**: + - Keep a per-thread reply for each unresolved thread + - Also prepare one overall review summary covering all addressed, clarified, and deferred items + - Keep broad rationale in the review summary and thread-specific details in the thread reply + +### Phase 5: Commit Changes + +1. **Stage changes by category** (prefer atomic commits): + + ```bash + git add + git commit -m "fix(scope): address review comment - + + Addresses review comment by @reviewer on PR #: + + + Changes: + - + - " + ``` + +2. **Alternative: Single commit for multiple related comments**: + + ```bash + git add -A + git commit -m "fix: address PR # review comments + + Addresses the following review feedback: + - @reviewer1: + - @reviewer2: + + Changes: + - + - + - " + ``` + +3. **Push changes**: + + ```bash + git push origin HEAD + ``` + +### Phase 6: Submit One Batched Review + +Do **not** post each reply individually unless batching is unavailable. Prefer one pending review, +attach all thread replies to it, then submit once so GitHub sends one grouped notification. + +1. **Create a pending review**: + + ```bash + # Replace PR_NODE_ID with the pull request GraphQL node id + gh api graphql -f query=' + mutation($pullRequestId: ID!) { + addPullRequestReview(input: {pullRequestId: $pullRequestId}) { + pullRequestReview { + id + } + } + } + ' -f pullRequestId=PR_NODE_ID + ``` + +2. **Add a reply for each unresolved thread to that pending review**: + + ```bash + # Replace REVIEW_ID and THREAD_ID with GraphQL node ids + gh api graphql -f query=' + mutation($reviewId: ID!, $threadId: ID!, $body: String!) { + addPullRequestReviewThreadReply( + input: { + pullRequestReviewId: $reviewId + pullRequestReviewThreadId: $threadId + body: $body + } + ) { + comment { + id + } + } + } + ' -f reviewId=REVIEW_ID -f threadId=THREAD_ID -f body='Addressed in commit abc1234. + - Added null check for empty input + - Updated tests to cover the edge case' + ``` + +3. **Submit the review once all thread replies are attached**: + + ```bash + gh api graphql -f query=' + mutation($reviewId: ID!, $body: String!) { + submitPullRequestReview( + input: { + pullRequestReviewId: $reviewId + event: COMMENT + body: $body + } + ) { + pullRequestReview { + id + url + } + } + } + ' -f reviewId=REVIEW_ID -f body='Addressed PR feedback in the linked commit(s). + + Summary: + - Resolved the requested code and test updates + - Added clarifications where code changes were not needed + - Deferred any out-of-scope items explicitly' + ``` + +4. **Fallback only if batching is unavailable**: + - Prefer GitHub MCP review tools if they support pending reviews and thread replies + - If review batching is not available, fall back to individual replies and warn that multiple notifications may be sent + - Avoid mixing batched review replies and individual replies unless the tooling forces it + +### Phase 7: Summary Report + +Output a summary: + +```markdown +## PR # Review Comments Addressed + +**Total unresolved comments**: X +**Addressed**: Y +**Deferred/Out of scope**: Z + +### Commits Created + +| Commit | Files | Comments Addressed | +| ------- | -------------------- | ------------------ | +| abc1234 | java/pom.xml | #1, #3 | +| def5678 | tests/integration.rs | #2 | + +### Review Submission + +- [x] Submitted one batched review for addressed threads +- [x] Included per-thread replies in the review +- [ ] Comment #3 by @reviewer3 - Deferred (created issue #XX) + +### Follow-up Items + +- Issue #XX: +``` + +## Error Handling + +- **Branch mismatch**: Stop and instruct user to checkout correct branch +- **Merge conflicts**: Stop and ask user to resolve conflicts first +- **Test failures**: Report which tests fail and ask for guidance +- **Unclear comments**: Ask for clarification before making changes +- **Permissions issues**: Report and suggest manual gh auth refresh +- **Batch review unsupported**: Fall back to individual replies only after stating that multiple notifications may be sent + +## Constitution Compliance + +This workflow MUST adhere to: + +- **Principle I (TDD)**: If adding functionality, write tests first +- **Principle IV (Clean Code)**: Ensure changes are readable and maintainable +- **Principle V (Security-First)**: Review any security implications of changes +- **Commit Hygiene**: GPG-signed commits with conventional commit messages +- **Branching Workflow**: Work on the correct feature branch + +## Example Usage + +```text +User: Address comments on PR 26 +Agent: +1. Fetching PR #26 details... +2. Found 3 unresolved review threads +3. Categorizing comments: + - Comment 1: Code change needed in java/pom.xml:36 + - Comment 2: Documentation update in docs/USAGE.md + - Comment 3: Clarification question (will reply) +4. Implementing fixes... +5. Running validation (fmt, clippy, tests)... +6. Committing changes... +7. Submitting one batched review... +8. Summary: 2 code changes committed, 1 clarification included in the batched review +``` + +## Quick Reference Commands + +```bash +# View PR details +gh pr view + +# List all comments +gh pr view --comments + +# Get review threads (GraphQL) +gh api graphql -f query='...' + +# Create a pending review +gh api repos/{owner}/{repo}/pulls/{pr}/reviews --method POST + +# Submit a pending review +gh api repos/{owner}/{repo}/pulls/{pr}/reviews/{review_id}/events --method POST -f event=COMMENT -f body="..." + +# Push and update PR +git push origin HEAD + +# Re-request review after addressing comments +gh pr edit --add-reviewer +``` diff --git a/api-tests/pom.xml b/api-tests/pom.xml index 4fb422a95..9bc835c85 100644 --- a/api-tests/pom.xml +++ b/api-tests/pom.xml @@ -13,12 +13,12 @@ UTF-8 4.3.4 4.3.4 - 7.33.0 - 1.5.18 + 7.34.2 + 1.5.32 UTF-8 4 - 2.19.2 + 2.21.1 17 (@Functional or @Smoke or @Performance) and not @Ignore @@ -29,45 +29,52 @@ 4.0.10 4.0.10 4.0.10 - 4.9.4 + 4.9.8 11.0.0 5.13.4 - 3.27.4 + 3.27.7 3.0 - 1.17.6 + 1.18.5 3.0.0 3.0.0 - 33.4.8-jre + 33.5.0-jre 20250517 - 4.2.8.Final - 4.2.3.Final - 4.2.3.Final - 5.5 + 4.2.10.Final + 4.2.10.Final + 4.2.10.Final + 5.6 2.12.2 - 1.19.0 + 1.21.0 6.2.9 2.3.34 - 2.13.1 + 2.13.2 5.5.5 0.9.275 - 1.18.38 + 1.18.42 - 4.9.3.2 - 12.1.9 + 4.9.8.2 + 12.2.0 2.13 3.6.0 - 3.5.3 - 3.5.3 - 3.14.0 - 4.6.17 - 3.27.0 + 3.5.5 + 3.5.5 + 3.15.0 + 4.6.20 + 3.28.0 3.0.5 - 3.5.1 + 3.6.3 + + com.fasterxml.jackson + jackson-bom + ${jackson-bom.version} + pom + import + org.junit junit-bom @@ -249,17 +256,14 @@ com.fasterxml.jackson.core jackson-core - ${jackson.version} com.fasterxml.jackson.core jackson-databind - ${jackson.version} com.fasterxml.jackson.core jackson-annotations - ${jackson.version} io.netty diff --git a/api-tests/src/test/java/com/amido/stacks/tests/api/CucumberTestSuite.java b/api-tests/src/test/java/com/amido/stacks/tests/api/CucumberTestSuite.java index 8ab260260..42902a5af 100644 --- a/api-tests/src/test/java/com/amido/stacks/tests/api/CucumberTestSuite.java +++ b/api-tests/src/test/java/com/amido/stacks/tests/api/CucumberTestSuite.java @@ -1,6 +1,7 @@ package com.amido.stacks.tests.api; import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; @@ -18,4 +19,7 @@ @DisabledIfSystemProperty(named = "untagged.test.check", matches = "true") @IncludeEngines("cucumber") @ConfigurationParameter(key = FEATURES_PROPERTY_NAME, value = "classpath:cucumber/features") +@ConfigurationParameter( + key = GLUE_PROPERTY_NAME, + value = "com.amido.stacks.tests.api.stepdefinitions") public class CucumberTestSuite {} diff --git a/build/azDevOps/azure/azure-pipelines-javaspring-k8s.yml b/build/azDevOps/azure/azure-pipelines-javaspring-k8s.yml index 4abac5ac6..e6bc70ad8 100644 --- a/build/azDevOps/azure/azure-pipelines-javaspring-k8s.yml +++ b/build/azDevOps/azure/azure-pipelines-javaspring-k8s.yml @@ -7,6 +7,13 @@ ############################################################################################################################# name: $(version_major).$(version_minor).$(version_patch)-$(Build.SourceBranchName)-$(Rev:r) +parameters: + - name: runVulnerabilityScan + displayName: Run OWASP Dependency Check + type: boolean + # Default on to preserve the existing security posture; opt out explicitly if a non-default branch flow requires it. + default: true + pr: - master @@ -138,7 +145,7 @@ variables: # Vulnerability Scan - name: vulnerability_scan - value: true + value: ${{ parameters.runVulnerabilityScan }} - name: vulnerability_scan_report value: "target/dependency-check-report.html" - name: oss_index_username @@ -228,7 +235,7 @@ stages: # Docker docker_build_container: "${{ variables.docker_java_image }}" # Vulnerability Scanning - vulnerability_scan: "${{ variables.vulnerability_scan }}" + vulnerability_scan: ${{ variables.vulnerability_scan }} nvd_api_key: "$(NVD_API_KEY)" oss_index_username: "${{ variables.oss_index_username }}" oss_index_password: "${{ variables.oss_index_password }}" @@ -253,7 +260,7 @@ stages: # Docker docker_build_container: "${{ variables.docker_java_image }}" # Vulnerability Scan - vulnerability_scan: "${{ variables.vulnerability_scan }}" + vulnerability_scan: ${{ variables.vulnerability_scan }} nvd_api_key: "$(NVD_API_KEY)" oss_index_username: "${{ variables.oss_index_username }}" oss_index_password: "${{ variables.oss_index_password }}" @@ -315,7 +322,7 @@ stages: functional_test: ${{ variables.functional_test }} functional_test_artefact_name: "${{ variables.functional_test_artefact_name }}" functional_test_artefact_path: "${{ variables.functional_test_artefact_path }}" - vulnerability_scan: "${{ variables.vulnerability_scan }}" + vulnerability_scan: ${{ variables.vulnerability_scan }} vulnerability_scan_report: "${{ variables.vulnerability_scan_report }}" java_project_type: "${{ variables.java_project_type }}" functional_test_project_type: "${{ variables.functional_test_project_type }}" @@ -337,11 +344,12 @@ stages: - group: stacks-acr-creds - group: stacks-infra-credentials-nonprod - group: stacks-credentials-nonprod-kv + - group: stacks-dev-outputs - group: stacks-java-api - name: dns_name value: "$(Environment.ShortName)-java-api" - name: infra_resource_group - value: $(tf_core_resource_group_nonprod) + value: $(resource_group_name) - name: Environment.ShortName value: dev - name: BUILD_ATTEMPT_NUMBER @@ -359,8 +367,6 @@ stages: value: "{}" - name: resource_group_location value: "$(region)" - - name: app_gateway_frontend_ip_name - value: $(tf_app_gateway_frontend_ip_name_nonprod) - name: create_cosmosdb value: false - name: create_cache @@ -369,8 +375,6 @@ stages: value: true - name: create_cdn_endpoint value: false - - name: app_insights_name - value: $(tf_app_insights_name_nonprod) strategy: runOnce: deploy: @@ -414,7 +418,6 @@ stages: TF_VAR_attributes: "${{ variables.attributes }}", TF_VAR_tags: "${{ variables.tags }}", TF_VAR_resource_group_location: "${{ variables.resource_group_location }}", - TF_VAR_app_gateway_frontend_ip_name: "${{ variables.app_gateway_frontend_ip_name }}", TF_VAR_dns_record: "${{ variables.dns_name }}", TF_VAR_dns_zone_name: "${{ variables.base_domain_nonprod }}", TF_VAR_dns_zone_resource_group: "${{ variables.dns_zone_resource_group }}", @@ -424,7 +427,10 @@ stages: TF_VAR_create_cache: "${{ variables.create_cache }}", TF_VAR_create_dns_record: "${{ variables.create_dns_record }}", TF_VAR_create_cdn_endpoint: "${{ variables.create_cdn_endpoint }}", - TF_VAR_app_insights_name: "${{ variables.app_insights_name }}", + TF_VAR_app_gateway_frontend_ip_name: "$(app_gateway_public_ip_name)", + TF_VAR_app_gateway_resource_group_name: "$(app_gateway_resource_group_name)", + TF_VAR_app_insights_name: "$(app_insights_name)", + TF_VAR_app_insights_resource_group_name: "$(app_insights_resource_group_name)", } terraform_output_commands: | raw_tf=$(terraform output -json | jq -r 'keys[] as $k | "##vso[task.setvariable variable=\($k);isOutput=true]\(.[$k] | .value)"') @@ -453,9 +459,9 @@ stages: - name: functional_test_base_url value: "https://${{ variables.dns_pointer }}${{ variables.k8s_app_path }}" - name: aks_cluster_resourcegroup - value: "${{ variables.infra_resource_group }}" + value: $(aks_resource_group_name) - name: aks_cluster_name - value: $(kubernetes_clustername_nonprod) + value: $(aks_cluster_name) - name: app_name value: "java-api" strategy: @@ -546,11 +552,12 @@ stages: - group: stacks-acr-creds - group: stacks-infra-credentials-prod - group: stacks-credentials-prod-kv + - group: stacks-prod-outputs - group: stacks-java-api - name: dns_name value: "$(Environment.ShortName)-java-api" - name: infra_resource_group - value: $(tf_core_resource_group_prod) + value: $(resource_group_name) - name: Environment.ShortName value: prod - name: BUILD_ATTEMPT_NUMBER @@ -562,10 +569,6 @@ stages: vmImage: $(pool_vm_image) environment: ${{ variables.domain }}-prod variables: - - name: app_insights_name - value: $(tf_app_insights_name_prod) - - name: app_gateway_frontend_ip_name - value: $(tf_app_gateway_frontend_ip_name_prod) - name: attributes value: "[]" - name: tags @@ -623,7 +626,6 @@ stages: TF_VAR_attributes: "${{ variables.attributes }}", TF_VAR_tags: "${{ variables.tags }}", TF_VAR_resource_group_location: "${{ variables.resource_group_location }}", - TF_VAR_app_gateway_frontend_ip_name: "${{ variables.app_gateway_frontend_ip_name }}", TF_VAR_dns_record: "${{ variables.dns_name }}", TF_VAR_dns_zone_name: "${{ variables.base_domain_prod }}", TF_VAR_dns_zone_resource_group: "${{ variables.dns_zone_resource_group }}", @@ -633,7 +635,10 @@ stages: TF_VAR_create_cache: "${{ variables.create_cache }}", TF_VAR_create_dns_record: "${{ variables.create_dns_record }}", TF_VAR_create_cdn_endpoint: "${{ variables.create_cdn_endpoint }}", - TF_VAR_app_insights_name: "${{ variables.app_insights_name }}", + TF_VAR_app_gateway_frontend_ip_name: "$(app_gateway_public_ip_name)", + TF_VAR_app_gateway_resource_group_name: "$(app_gateway_resource_group_name)", + TF_VAR_app_insights_name: "$(app_insights_name)", + TF_VAR_app_insights_resource_group_name: "$(app_insights_resource_group_name)", } terraform_output_commands: | raw_tf=$(terraform output -json | jq -r 'keys[] as $k | "##vso[task.setvariable variable=\($k);isOutput=true]\(.[$k] | .value)"') @@ -703,9 +708,9 @@ stages: - name: functional_test_base_url value: "https://${{ variables.dns_pointer }}/${{ variables.k8s_app_path }}" - name: aks_cluster_resourcegroup - value: "${{ variables.infra_resource_group }}" + value: $(aks_resource_group_name) - name: aks_cluster_name - value: $(kubernetes_clustername_prod) + value: $(aks_cluster_name) - name: app_name value: "java-api" strategy: diff --git a/build/azDevOps/azure/azuredevops-vars.yml b/build/azDevOps/azure/azuredevops-vars.yml index 837ea9637..f24136e19 100644 --- a/build/azDevOps/azure/azuredevops-vars.yml +++ b/build/azDevOps/azure/azuredevops-vars.yml @@ -32,31 +32,9 @@ variables: - name: tf_state_key value: stacks-api-java - # TF Variables - # --nonprod - - name: tf_app_insights_name_nonprod - value: ed-stacks-nonprod-euw-core - - name: tf_app_gateway_frontend_ip_name_nonprod - value: ed-stacks-nonprod-euw-core - - name: tf_core_resource_group_nonprod - value: ed-stacks-nonprod-euw-core - - # --prod - - name: tf_app_insights_name_prod - value: ed-stacks-prod-euw-core - - name: tf_app_gateway_frontend_ip_name_prod - value: ed-stacks-prod-euw-core - - name: tf_core_resource_group_prod - value: ed-stacks-prod-euw-core - - # Kubernetes configuration - # --nonprod - - name: kubernetes_clustername_nonprod - value: ed-stacks-nonprod-euw-core - - # --prod - - name: kubernetes_clustername_prod - value: ed-stacks-prod-euw-core + # AKS-integrated deployment values now come from Azure DevOps Library variable groups + # created by stacks-infrastructure-aks, for example stacks-dev-outputs/stacks-prod-outputs. + # Keep repo-local defaults here only for values that are static across environments. # Container registry configuration - name: docker_container_registry_name_nonprod diff --git a/build/azDevOps/azure/coverage/package-lock.json b/build/azDevOps/azure/coverage/package-lock.json index 30a9bb79f..acc17d6ae 100644 --- a/build/azDevOps/azure/coverage/package-lock.json +++ b/build/azDevOps/azure/coverage/package-lock.json @@ -270,16 +270,6 @@ "node": ">=10" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -1443,6 +1433,21 @@ "node": ">=10.13.0" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3168,6 +3173,16 @@ "dev": true, "license": "MIT" }, + "node_modules/sax": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -3418,19 +3433,19 @@ } }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" diff --git a/deploy/azure/app/kube/app_insights.tf b/deploy/azure/app/kube/app_insights.tf index b2cab3879..4f1b01dbe 100644 --- a/deploy/azure/app/kube/app_insights.tf +++ b/deploy/azure/app/kube/app_insights.tf @@ -1,6 +1,6 @@ # Example of further extensions to Stacks Core templates # Potential user defined extensions data "azurerm_application_insights" "example" { - name = var.infra_resource_group - resource_group_name = var.app_insights_name + name = var.app_insights_name + resource_group_name = var.app_insights_resource_group_name != "" ? var.app_insights_resource_group_name : var.infra_resource_group } diff --git a/deploy/azure/app/kube/main.tf b/deploy/azure/app/kube/main.tf index 5850e6ddc..def1c85e8 100644 --- a/deploy/azure/app/kube/main.tf +++ b/deploy/azure/app/kube/main.tf @@ -35,6 +35,6 @@ module "app" { infra_resource_group = var.infra_resource_group dns_zone_resource_group = var.dns_zone_resource_group != "" ? var.dns_zone_resource_group : var.infra_resource_group dns_ip_address_name = var.app_gateway_frontend_ip_name - dns_ip_address_resource_group = var.infra_resource_group + dns_ip_address_resource_group = var.app_gateway_resource_group_name != "" ? var.app_gateway_resource_group_name : var.infra_resource_group subscription_id = data.azurerm_client_config.current.subscription_id } diff --git a/deploy/azure/app/kube/variables.tf b/deploy/azure/app/kube/variables.tf index 1ce5627c5..8c9f28aa0 100644 --- a/deploy/azure/app/kube/variables.tf +++ b/deploy/azure/app/kube/variables.tf @@ -63,6 +63,12 @@ variable "app_gateway_frontend_ip_name" { type = string } +variable "app_gateway_resource_group_name" { + type = string + description = "Resource group containing the Application Gateway public IP" + default = "" +} + variable "dns_record" { type = string } @@ -137,3 +143,9 @@ variable "app_insights_name" { type = string description = "app insights name for key retriaval in memory" } + +variable "app_insights_resource_group_name" { + type = string + description = "Resource group containing the Application Insights instance" + default = "" +} diff --git a/docs/spring-boot-3.5-migration.md b/docs/spring-boot-3.5-migration.md index aee1a94b6..62e986587 100644 --- a/docs/spring-boot-3.5-migration.md +++ b/docs/spring-boot-3.5-migration.md @@ -11,23 +11,21 @@ The `stacks-modules-parent:3.0.98` brings in Spring Boot 3.5.7, which introduces **Problem:** The current Spring Cloud version (`2022.0.4`) is incompatible with Spring Boot 3.5.7. -``` +```text Spring Boot [3.5.7] is not compatible with this Spring Cloud release train. Change Spring Boot version to one of the following versions [3.0.x, 3.1.x]. ``` **Required Fix in Parent POM:** -Update `spring.cloud.dependencies.version` to a version compatible with Spring Boot 3.5.x: +Update `spring.cloud.dependencies.version` to a release train that Spring Boot 3.5.x accepts without disabling the verifier: -| Spring Boot Version | Compatible Spring Cloud Version | -| ------------------- | ------------------------------- | -| 3.0.x, 3.1.x | 2022.0.x (Kilburn) | -| 3.2.x | 2023.0.x (Leyton) | -| 3.3.x, 3.4.x | 2024.0.x | -| 3.5.x | 2025.0.x | +- Spring Boot 3.0.x / 3.1.x: Spring Cloud 2022.0.x (Kilburn) +- Spring Boot 3.2.x: Spring Cloud 2023.0.x (Leyton) +- Spring Boot 3.3.x / 3.4.x: Spring Cloud 2024.0.x +- Spring Boot 3.5.x: this repository is temporarily pinned to Spring Cloud 2024.0.3 and disables the compatibility verifier until upstream support catches up **Workaround (current):** -Projects can disable the compatibility verifier in `application-test.yml`: +This repository currently disables the compatibility verifier in `application.yml` so the application can start while the parent POM and Spring Cloud release train catch up: ```yaml spring: @@ -36,7 +34,7 @@ spring: enabled: false ``` -**Action Required:** Update parent POM to use Spring Cloud 2024.0.x or later (once 2025.0.x is available for Spring Boot 3.5.x support). +**Action Required:** Move this repository to a Spring Cloud train that passes the compatibility verifier with Spring Boot 3.5.x, then remove the global `spring.cloud.compatibility-verifier.enabled=false` workaround. The repo is currently pinned to `2024.0.3`, which still requires the verifier workaround at runtime. --- @@ -45,7 +43,7 @@ spring: **Problem:** Spring Boot 3.5.x has stricter validation for Spring Security filter chains. Multiple `SecurityFilterChain` beans matching "any request" now throw an error: -``` +```text UnreachableFilterChainException: A filter chain that matches any request [...ApplicationConfig...] has already been configured, which means that this filter chain [...ApplicationNoSecurity...] will never get invoked. @@ -81,7 +79,7 @@ public class ApplicationNoSecurity { **Problem:** Spring Boot 3.5.x has stricter bean resolution when multiple beans of the same type exist through inheritance: -``` +```text NoUniqueBeanDefinitionException: expected single matching bean but found 2: menuService, menuServiceV2 ``` @@ -111,7 +109,7 @@ public class MenuServiceV2 extends MenuService { **Problem:** Property placeholders like `@aws.profile.name@` in `application.yml` are not being replaced because Maven resource filtering is not enabled by default. -``` +```text Profile '@aws.profile.name@' must start and end with a letter or digit ``` @@ -149,9 +147,9 @@ Enable resource filtering in `pom.xml`: ### Recommended (Should Add) -2. **Add default resource filtering configuration** so child projects don't need to configure it manually +1. **Add default resource filtering configuration** so child projects don't need to configure it manually -3. **Update documentation** to note the following breaking changes for downstream projects: +2. **Update documentation** to note the following breaking changes for downstream projects: - Security filter chain mutual exclusivity requirements - Bean resolution changes for inheritance hierarchies - Profile annotation requirements for conditional configurations @@ -160,12 +158,10 @@ Enable resource filtering in `pom.xml`: Until the parent POM is updated, the following workarounds have been applied: -| Issue | Workaround | File | -|--------------------------------|---------------------------------|-------------------------------------------| -| Spring Cloud incompatibility | Disabled compatibility verifier | `src/test/resources/application-test.yml` | -| Security filter chain conflict | Added `@Profile("!test")` | `ApplicationConfig.java` | -| Bean resolution conflict | Added `@Primary` | `MenuService.java` | -| Resource filtering | Added filtering config | `pom.xml` | +- Spring Cloud incompatibility: pin the BOM to `2024.0.3` and disable the compatibility verifier globally until an officially compatible train is available. Files: `java/pom.xml`, `java/src/main/resources/application.yml` +- Security filter chain conflict: added `@Profile("!test")`. File: `ApplicationConfig.java` +- Bean resolution conflict: added `@Primary`. File: `MenuService.java` +- Resource filtering: recommended to add filtering config in the parent POM; not yet applied in `java/pom.xml`. ## Testing Verification diff --git a/java/pom.xml b/java/pom.xml index 87c03022c..94cbd91f3 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.ensono.stacks.modules stacks-modules-parent - 3.0.111 + 3.0.139 com.amido.stacks.workloads @@ -28,15 +28,15 @@ 2.6.4 4.0.0 4.0.10 - 4.6.17 - 4.6.17 - 1.12.788 + 4.6.19 + 4.6.19 + 1.12.797 1.9.9.1 - 3.5.1 - 2025.0.0 + 3.6.3 + 2024.0.3 3.5.24 3.5.2 - 12.1.9 + 12.2.0 5.13.4 1.13.4 @@ -398,7 +398,7 @@ org.pitest pitest-junit5-plugin - 1.2.1 + 1.2.3 org.junit.platform diff --git a/java/src/main/java/com/amido/stacks/workloads/menu/mappers/CategoryMapper.java b/java/src/main/java/com/amido/stacks/workloads/menu/mappers/CategoryMapper.java index e4172ee0a..c94577b68 100644 --- a/java/src/main/java/com/amido/stacks/workloads/menu/mappers/CategoryMapper.java +++ b/java/src/main/java/com/amido/stacks/workloads/menu/mappers/CategoryMapper.java @@ -3,11 +3,13 @@ import com.amido.stacks.core.mapping.BaseMapper; import com.amido.stacks.workloads.menu.api.v1.dto.response.CategoryDTO; import com.amido.stacks.workloads.menu.domain.Category; +import org.mapstruct.InjectionStrategy; import org.mapstruct.Mapper; import org.mapstruct.NullValueCheckStrategy; @Mapper( componentModel = "spring", uses = {ItemMapper.class}, + injectionStrategy = InjectionStrategy.CONSTRUCTOR, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS) public interface CategoryMapper extends BaseMapper {} diff --git a/java/src/main/java/com/amido/stacks/workloads/menu/mappers/MenuMapper.java b/java/src/main/java/com/amido/stacks/workloads/menu/mappers/MenuMapper.java index 001a331c6..d3e995335 100644 --- a/java/src/main/java/com/amido/stacks/workloads/menu/mappers/MenuMapper.java +++ b/java/src/main/java/com/amido/stacks/workloads/menu/mappers/MenuMapper.java @@ -4,11 +4,13 @@ import com.amido.stacks.core.mapping.MapperUtils; import com.amido.stacks.workloads.menu.api.v1.dto.response.MenuDTO; import com.amido.stacks.workloads.menu.domain.Menu; +import org.mapstruct.InjectionStrategy; import org.mapstruct.Mapper; import org.mapstruct.NullValueCheckStrategy; @Mapper( componentModel = "spring", uses = {MapperUtils.class, CategoryMapper.class}, + injectionStrategy = InjectionStrategy.CONSTRUCTOR, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS) public interface MenuMapper extends BaseMapper {} diff --git a/java/src/main/java/com/amido/stacks/workloads/menu/mappers/SearchMenuResultItemMapper.java b/java/src/main/java/com/amido/stacks/workloads/menu/mappers/SearchMenuResultItemMapper.java index 6536061f6..9ffb67ad7 100644 --- a/java/src/main/java/com/amido/stacks/workloads/menu/mappers/SearchMenuResultItemMapper.java +++ b/java/src/main/java/com/amido/stacks/workloads/menu/mappers/SearchMenuResultItemMapper.java @@ -5,10 +5,21 @@ import com.amido.stacks.workloads.menu.api.v1.dto.response.SearchMenuResultItem; import com.amido.stacks.workloads.menu.domain.Menu; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; import org.mapstruct.NullValueCheckStrategy; @Mapper( componentModel = "spring", - uses = {MapperUtils.class, CategoryMapper.class}, + uses = {MapperUtils.class}, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS) -public interface SearchMenuResultItemMapper extends BaseMapper {} +public interface SearchMenuResultItemMapper extends BaseMapper { + + @Override + @Mapping(target = "categories", ignore = true) + Menu fromDto(SearchMenuResultItem dto); + + @Override + @Mapping(target = "categories", ignore = true) + void updateFromDto(SearchMenuResultItem dto, @MappingTarget Menu menu); +} diff --git a/java/src/main/resources/application.yml b/java/src/main/resources/application.yml index 60c4b3d60..113d47cec 100644 --- a/java/src/main/resources/application.yml +++ b/java/src/main/resources/application.yml @@ -6,6 +6,9 @@ spring: application: name: stacks-api + cloud: + compatibility-verifier: + enabled: false data: rest: detection-strategy: annotated diff --git a/java/src/test/java/com/amido/stacks/workloads/actuator/ActuatorTest.java b/java/src/test/java/com/amido/stacks/workloads/actuator/ActuatorTest.java index db8f210dc..d5193c0fc 100644 --- a/java/src/test/java/com/amido/stacks/workloads/actuator/ActuatorTest.java +++ b/java/src/test/java/com/amido/stacks/workloads/actuator/ActuatorTest.java @@ -12,12 +12,14 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(properties = {"management.port=0"}) @EnableAutoConfiguration @Tag("Component") +@ActiveProfiles("test") class ActuatorTest { @Value("${local.management.port}") diff --git a/java/src/test/java/com/amido/stacks/workloads/menu/mappers/DomainToDtoMapperMapstructTest.java b/java/src/test/java/com/amido/stacks/workloads/menu/mappers/DomainToDtoMapperMapstructTest.java index 9b4a573b2..7d1a56997 100644 --- a/java/src/test/java/com/amido/stacks/workloads/menu/mappers/DomainToDtoMapperMapstructTest.java +++ b/java/src/test/java/com/amido/stacks/workloads/menu/mappers/DomainToDtoMapperMapstructTest.java @@ -15,30 +15,26 @@ import java.util.UUID; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; @Tag("Unit") -@SpringBootTest( - classes = { - MenuMapper.class, - MenuMapperImpl.class, - CategoryMapper.class, - CategoryMapperImpl.class, - ItemMapper.class, - ItemMapperImpl.class, - SearchMenuResultItemMapper.class, - SearchMenuResultItemMapperImpl.class - }) class DomainToDtoMapperMapstructTest { - @Autowired private MenuMapper menuMapper; + private final MenuMapper menuMapper; - @Autowired private CategoryMapper categoryMapper; + private final CategoryMapper categoryMapper; - @Autowired private ItemMapper itemMapper; + private final ItemMapper itemMapper; - @Autowired private SearchMenuResultItemMapper searchMenuResultItemMapper; + private final SearchMenuResultItemMapper searchMenuResultItemMapper; + + DomainToDtoMapperMapstructTest() { + itemMapper = new ItemMapperImpl(); + + categoryMapper = new CategoryMapperImpl(itemMapper); + menuMapper = new MenuMapperImpl(categoryMapper); + + searchMenuResultItemMapper = new SearchMenuResultItemMapperImpl(); + } @Test void menuToMenuDto() { diff --git a/java/src/test/resources/application-test.yml b/java/src/test/resources/application-test.yml new file mode 100644 index 000000000..650e48484 --- /dev/null +++ b/java/src/test/resources/application-test.yml @@ -0,0 +1,6 @@ +spring: + cloud: + compatibility-verifier: + enabled: false + config: + enabled: false