diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 436e1fa0aa..75b1aa73b6 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -35,7 +35,7 @@ runs: - name: get latest tag sha if: ${{ inputs.full-setup == 'true' }} id: tag-sha - uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@98bcfbe06aafffdc0e9a790f352602316f82303b # v18.23.0 + uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@567a9393acca033008ce34eafcb200469e4ea2f5 # v18.27.0 - name: load "NPM TAG" if: ${{ inputs.full-setup == 'true' }} id: set-npm-tag diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 10ca901c09..8889621040 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -1,9 +1,9 @@ { "entries": { - "github/gh-aw-actions/setup@v0.85.4": { + "github/gh-aw-actions/setup@v0.87.10": { "repo": "github/gh-aw-actions/setup", - "version": "v0.85.4", - "sha": "2709137ea6c5b0e19aa621454dc643ea8dc526b1" + "version": "v0.87.10", + "sha": "bc8c008a419c5b7a29df6f5641edd35fd1c6ea85" } }, "containers": { @@ -22,20 +22,10 @@ "digest": "sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627", "pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627" }, - "ghcr.io/github/gh-aw-mcpg:v0.4.8": { - "image": "ghcr.io/github/gh-aw-mcpg:v0.4.8", - "digest": "sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8", - "pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8" - }, "ghcr.io/github/gh-aw-node": { "image": "ghcr.io/github/gh-aw-node", "digest": "sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196", "pinned_image": "ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196" - }, - "ghcr.io/github/github-mcp-server:v1.8.0": { - "image": "ghcr.io/github/github-mcp-server:v1.8.0", - "digest": "sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520", - "pinned_image": "ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520" } } } diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9a2c2a18d4..9bfd7bbe95 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,7 +30,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v3.29.5 + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v3.29.5 # Override language selection by uncommenting this and choosing your languages with: languages: javascript @@ -39,7 +39,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v3.29.5 + uses: github/codeql-action/autobuild@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v3.29.5 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -53,4 +53,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v3.29.5 + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v3.29.5 diff --git a/.github/workflows/notify-on-an-bdu-label.yml b/.github/workflows/notify-on-an-bdu-label.yml index cafc0dff62..dbc218d687 100644 --- a/.github/workflows/notify-on-an-bdu-label.yml +++ b/.github/workflows/notify-on-an-bdu-label.yml @@ -53,7 +53,7 @@ jobs: - name: Send Teams notification if: steps.check_label_timing.outputs.should_notify == 'true' - uses: Alfresco/alfresco-build-tools/.github/actions/send-teams-notification@98bcfbe06aafffdc0e9a790f352602316f82303b # v18.23.0 + uses: Alfresco/alfresco-build-tools/.github/actions/send-teams-notification@567a9393acca033008ce34eafcb200469e4ea2f5 # v18.27.0 with: webhook-url: ${{ secrets.TEAMS_NOTIFICATION_ADF_BDU_WEBHOOK }} skip_checkout: true diff --git a/.github/workflows/pull-from-crowdin.yml b/.github/workflows/pull-from-crowdin.yml index 6e6feb074d..51ff36ad06 100644 --- a/.github/workflows/pull-from-crowdin.yml +++ b/.github/workflows/pull-from-crowdin.yml @@ -29,7 +29,7 @@ jobs: ref: develop token: ${{ steps.app-token.outputs.token }} - name: Pull translations from Crowdin - uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0 + uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1 with: skip_ref_checkout: true upload_sources: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3c66cb632..a5afc3eae1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -147,7 +147,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Push Source Files to Crowdin - uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0 + uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1 with: upload_sources: true upload_sources_args: --delete-obsolete diff --git a/.github/workflows/stale-pr-cleanup.yml b/.github/workflows/stale-pr-cleanup.yml index e14f9155c7..3bc400938d 100644 --- a/.github/workflows/stale-pr-cleanup.yml +++ b/.github/workflows/stale-pr-cleanup.yml @@ -11,4 +11,4 @@ permissions: jobs: stale-pr-cleanup: - uses: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml@98bcfbe06aafffdc0e9a790f352602316f82303b # v18.23.0 + uses: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml@567a9393acca033008ce34eafcb200469e4ea2f5 # v18.27.0 diff --git a/.github/workflows/supply-chain-pr-instructions.yml b/.github/workflows/supply-chain-pr-instructions.yml index 5c5166f57f..40207f4e8b 100644 --- a/.github/workflows/supply-chain-pr-instructions.yml +++ b/.github/workflows/supply-chain-pr-instructions.yml @@ -16,7 +16,7 @@ jobs: permissions: pull-requests: write steps: - - uses: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment@98bcfbe06aafffdc0e9a790f352602316f82303b # v18.23.0 + - uses: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment@567a9393acca033008ce34eafcb200469e4ea2f5 # v18.27.0 with: comment-identifier: supply-chain-review-instructions comment-body: | diff --git a/.github/workflows/supply-chain-review.lock.yml b/.github/workflows/supply-chain-review.lock.yml index 8a114a76b5..c0f357e815 100644 --- a/.github/workflows/supply-chain-review.lock.yml +++ b/.github/workflows/supply-chain-review.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e67df013a0fc1bd33c740d2f2bc528e20099bd6fabb6a0d4df4600c2a99a679d","body_hash":"57d96c74e7e29ae4c2b4c5e5c3355c9310da41fb777d9322e65dfb41e3a6f5f6","compiler_version":"v0.85.4","strict":true,"agent_id":"copilot","agent_model":"gpt-5-mini","engine_versions":{"copilot":"1.0.78"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"2709137ea6c5b0e19aa621454dc643ea8dc526b1","version":"v0.85.4"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.8","digest":"sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} -# This file was automatically generated by gh-aw (v0.85.4). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"595a2c9a8f52011756fe3d1d6162d85d9ce9601e6c762da917f00264507efd39","body_hash":"57d96c74e7e29ae4c2b4c5e5c3355c9310da41fb777d9322e65dfb41e3a6f5f6","compiler_version":"v0.87.10","strict":true,"agent_id":"copilot","agent_model":"gpt-5-mini","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"bc8c008a419c5b7a29df6f5641edd35fd1c6ea85","version":"v0.87.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.10","digest":"sha256:c01e6d16d11ea4f2a46cc023a9f402224a3b3861b026818eec0dc586d7e6918e","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.10@sha256:c01e6d16d11ea4f2a46cc023a9f402224a3b3861b026818eec0dc586d7e6918e"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.10","digest":"sha256:c3a18aebb8251339117ea998296315de17bada366f8d03919b3348ea71112e64","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.10@sha256:c3a18aebb8251339117ea998296315de17bada366f8d03919b3348ea71112e64"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.10","digest":"sha256:c06076f7aca95df713e0748c44d80c0a3c2538fad67bfdd04296d45158e083e6","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.10@sha256:c06076f7aca95df713e0748c44d80c0a3c2538fad67bfdd04296d45158e083e6"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.14","digest":"sha256:b2f0c2b2f17b5fbe809e5bb99dc185b6ddd70df25295dc63a6d526350334eff5","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.14@sha256:b2f0c2b2f17b5fbe809e5bb99dc185b6ddd70df25295dc63a6d526350334eff5"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","list_branches","list_commits","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","missing_data","missing_tool","noop","remove_labels","submit_pull_request_review"]}]} +# This file was automatically generated by gh-aw (v0.87.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -17,17 +17,18 @@ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # # -# To update this file, edit Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f and run: +# To update this file, edit Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@e35840d877477896b1f0aa05d05371cb3b31ce9f and run: # gh aw compile # Not all edits will cause changes to this file. # # For more information: https://github.github.com/gh-aw/introduction/overview/ # # -# Source: Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f +# Source: Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@e35840d877477896b1f0aa05d05371cb3b31ce9f # # Secrets used: # - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -39,17 +40,16 @@ # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2 +# - github/gh-aw-actions/setup@bc8c008a419c5b7a29df6f5641edd35fd1c6ea85 # v0.87.10 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7 -# - ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627 -# - ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.10@sha256:c01e6d16d11ea4f2a46cc023a9f402224a3b3861b026818eec0dc586d7e6918e +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.10@sha256:c3a18aebb8251339117ea998296315de17bada366f8d03919b3348ea71112e64 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.10@sha256:c06076f7aca95df713e0748c44d80c0a3c2538fad67bfdd04296d45158e083e6 +# - ghcr.io/github/gh-aw-mcpg:v0.4.14@sha256:b2f0c2b2f17b5fbe809e5bb99dc185b6ddd70df25295dc63a6d526350334eff5 # - ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 -# - ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520 +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Supply Chain Review" on: @@ -84,9 +84,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}" + queue: max run-name: "Supply Chain Review" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.supply-chain-review + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Supply%20Chain%20Review,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: needs: pre_activation @@ -125,7 +134,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -135,37 +144,42 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Supply Chain Review" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/supply-chain-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.78" - GH_AW_INFO_AWF_VERSION: "v0.27.44" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.10" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Generate agentic run info id: generate_aw_info env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: "gpt-5-mini" - GH_AW_INFO_VERSION: "1.0.78" - GH_AW_INFO_AGENT_VERSION: "1.0.78" - GH_AW_INFO_CLI_VERSION: "v0.85.4" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.87.10" GH_AW_INFO_WORKFLOW_NAME: "Supply Chain Review" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","node","java","api.osv.dev","api.scorecard.dev","search.maven.org"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.44" + GH_AW_INFO_AWF_VERSION: "v0.28.10" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" - GH_AW_INFO_FRONTMATTER_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f" + GH_AW_INFO_AGENT_RUNTIME: "" + GH_AW_INFO_FRONTMATTER_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@e35840d877477896b1f0aa05d05371cb3b31ce9f" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -187,9 +201,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -207,9 +223,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Add rocket reaction for immediate feedback id: react @@ -220,9 +238,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/add_reaction.cjs'); + const { main } = require(path.join(actionsDir, 'add_reaction.cjs')); await main(); - name: Validate COPILOT_GITHUB_TOKEN secret id: validate-secret @@ -253,8 +273,8 @@ jobs: env: GH_AW_AGENT_FOLDERS: ".agents .github" GH_AW_AGENT_FILES: "AGENTS.md" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -263,30 +283,36 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.85.4" + GH_AW_COMPILED_VERSION: "v0.87.10" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Compute current body text id: sanitized uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,adoptium.net,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.osv.dev,api.scorecard.dev,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,develocity.apache.org,dl.google.com,dlcdn.apache.org,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,ge.spockframework.org,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,host.docker.internal,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,search.maven.org,security.ubuntu.com,services.gradle.org,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,adoptium.net,api.adoptium.net,api.foojay.io,api.npms.io,api.osv.dev,api.scorecard.dev,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,develocity.apache.org,dl.google.com,dlcdn.apache.org,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,ge.spockframework.org,get.pnpm.io,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,search.maven.org,security.ubuntu.com,services.gradle.org,skimdb.npmjs.com,storage.googleapis.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + const { main } = require(path.join(actionsDir, 'compute_text.cjs')); await main(); - name: Add comment with workflow run link id: add-comment @@ -296,17 +322,22 @@ jobs: GH_AW_WORKFLOW_NAME: "Supply Chain Review" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/add_workflow_run_comment.cjs'); + const { main } = require(path.join(actionsDir, 'add_workflow_run_comment.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"file\":\"pr_context_prompt.md\",\"condition_env\":\"GH_AW_INCLUDE_PR_CONTEXT\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -315,78 +346,36 @@ jobs: GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_c48f5cf53b906b4c_EOF' - - GH_AW_PROMPT_c48f5cf53b906b4c_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_c48f5cf53b906b4c_EOF' - - Tools: add_comment, submit_pull_request_review, add_labels, remove_labels, missing_tool, missing_data, noop - - GH_AW_PROMPT_c48f5cf53b906b4c_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_c48f5cf53b906b4c_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_c48f5cf53b906b4c_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then - cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" - fi - cat << 'GH_AW_PROMPT_c48f5cf53b906b4c_EOF' - - {{#runtime-import .github/workflows/supply-chain-review.md}} - GH_AW_PROMPT_c48f5cf53b906b4c_EOF - } > "$GH_AW_PROMPT" + GH_AW_INCLUDE_PR_CONTEXT: ${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) || github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review' }} + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, submit_pull_request_review, add_labels, remove_labels, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/supply-chain-review.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs')); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -395,16 +384,18 @@ jobs: GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }} + GH_AW_INCLUDE_PR_CONTEXT: ${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) || github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review' }} GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -418,7 +409,7 @@ jobs: GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_IS_PR_COMMENT: process.env.GH_AW_IS_PR_COMMENT, + GH_AW_INCLUDE_PR_CONTEXT: process.env.GH_AW_INCLUDE_PR_CONTEXT, GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND @@ -426,16 +417,20 @@ jobs: }); - name: Validate prompt placeholders env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -460,12 +455,19 @@ jobs: permissions: contents: read pull-requests: read + timeout-minutes: 60 env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: supplychainreview outputs: @@ -490,11 +492,12 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -503,18 +506,25 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Supply Chain Review" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/supply-chain-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.78" - GH_AW_INFO_AWF_VERSION: "v0.27.44" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.10" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${{ runner.tool_cache }}" >> "$GITHUB_ENV" + fi { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -546,17 +556,19 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - GH_AW_COMPILED_VERSION: v0.85.4 + GH_AW_COMPILED_VERSION: v0.87.10 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.44 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.10 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -565,7 +577,9 @@ jobs: GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' @@ -583,15 +597,26 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7 ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627 ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8 ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.10@sha256:c01e6d16d11ea4f2a46cc023a9f402224a3b3861b026818eec0dc586d7e6918e ghcr.io/github/gh-aw-firewall/api-proxy:0.28.10@sha256:c3a18aebb8251339117ea998296315de17bada366f8d03919b3348ea71112e64 ghcr.io/github/gh-aw-firewall/squid:0.28.10@sha256:c06076f7aca95df713e0748c44d80c0a3c2538fad67bfdd04296d45158e083e6 ghcr.io/github/gh-aw-mcpg:v0.4.14@sha256:b2f0c2b2f17b5fbe809e5bb99dc185b6ddd70df25295dc63a6d526350334eff5 ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_e7c09bf25de03137_EOF' - {"add_comment":{"hide_older_comments":true,"max":1},"add_labels":{"allowed":["security:low","security:medium","security:high"]},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"remove_labels":{"allowed":["security:low","security:medium","security:high"]},"report_incomplete":{},"submit_pull_request_review":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_e7c09bf25de03137_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"security:low\",\"security:medium\",\"security:high\"],\"issue_intent\":false},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"remove_labels\":{\"allowed\":[\"security:low\",\"security:medium\",\"security:high\"]},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -616,9 +641,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -626,6 +660,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -763,9 +807,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -782,33 +828,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.8' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.14' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_27dbc4d32b15e3d5_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_5937beb445feba84_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.8.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -841,6 +899,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -860,25 +926,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_27dbc4d32b15e3d5_EOF + GH_AW_MCP_CONFIG_5937beb445feba84_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -890,22 +963,34 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 + timeout-minutes: ${{ fromJSON(vars.GH_AW_DEFAULT_TIMEOUT_MINUTES || '20') }} run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.44/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.gradle-enterprise.cloud\",\"adoptium.net\",\"api.adoptium.net\",\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.foojay.io\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.npms.io\",\"api.osv.dev\",\"api.scorecard.dev\",\"api.snapcraft.io\",\"archive.apache.org\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"bun.sh\",\"cdn.azul.com\",\"cdn.jsdelivr.net\",\"central.sonatype.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"deb.nodesource.com\",\"deno.land\",\"develocity.apache.org\",\"dl.google.com\",\"dlcdn.apache.org\",\"download.eclipse.org\",\"download.java.net\",\"download.oracle.com\",\"downloads.gradle-dn.com\",\"esm.sh\",\"ge.spockframework.org\",\"get.pnpm.io\",\"github.com\",\"googleapis.deno.dev\",\"googlechromelabs.github.io\",\"gradle.org\",\"host.docker.internal\",\"jcenter.bintray.com\",\"jdk.java.net\",\"json-schema.org\",\"json.schemastore.org\",\"jsr.io\",\"keyserver.ubuntu.com\",\"maven-central.storage-download.googleapis.com\",\"maven.apache.org\",\"maven.google.com\",\"maven.oracle.com\",\"maven.pkg.github.com\",\"nodejs.org\",\"npm.pkg.github.com\",\"npmjs.com\",\"npmjs.org\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"plugins-artifacts.gradle.org\",\"plugins.gradle.org\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.bower.io\",\"registry.npmjs.com\",\"registry.npmjs.org\",\"registry.yarnpkg.com\",\"repo.gradle.org\",\"repo.grails.org\",\"repo.maven.apache.org\",\"repo.spring.io\",\"repo.yarnpkg.com\",\"repo1.maven.org\",\"repository.apache.org\",\"s.symcb.com\",\"s.symcd.com\",\"scans-in.gradle.com\",\"search.maven.org\",\"security.ubuntu.com\",\"services.gradle.org\",\"skimdb.npmjs.com\",\"storage.googleapis.com\",\"telemetry.enterprise.githubcopilot.com\",\"telemetry.vercel.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\",\"www.java.com\",\"www.npmjs.com\",\"www.npmjs.org\",\"yarnpkg.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.44,squid=sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627,agent=sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4,api-proxy=sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7,cli-proxy=sha256:c064d15974f7c933ec7d3f7b4038f4fd203547b3154bdc821afd379144887eff\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.10/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.gradle-enterprise.cloud\",\"adoptium.net\",\"api.adoptium.net\",\"api.foojay.io\",\"api.npms.io\",\"api.osv.dev\",\"api.scorecard.dev\",\"api.snapcraft.io\",\"archive.apache.org\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"bun.sh\",\"cdn.azul.com\",\"cdn.jsdelivr.net\",\"central.sonatype.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"deb.nodesource.com\",\"deno.land\",\"develocity.apache.org\",\"dl.google.com\",\"dlcdn.apache.org\",\"download.eclipse.org\",\"download.java.net\",\"download.oracle.com\",\"downloads.gradle-dn.com\",\"esm.sh\",\"ge.spockframework.org\",\"get.pnpm.io\",\"googleapis.deno.dev\",\"googlechromelabs.github.io\",\"gradle.org\",\"jcenter.bintray.com\",\"jdk.java.net\",\"json-schema.org\",\"json.schemastore.org\",\"jsr.io\",\"keyserver.ubuntu.com\",\"maven-central.storage-download.googleapis.com\",\"maven.apache.org\",\"maven.google.com\",\"maven.oracle.com\",\"maven.pkg.github.com\",\"nodejs.org\",\"npm.pkg.github.com\",\"npmjs.com\",\"npmjs.org\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"plugins-artifacts.gradle.org\",\"plugins.gradle.org\",\"ppa.launchpad.net\",\"registry.bower.io\",\"registry.npmjs.com\",\"registry.npmjs.org\",\"registry.yarnpkg.com\",\"repo.gradle.org\",\"repo.grails.org\",\"repo.maven.apache.org\",\"repo.spring.io\",\"repo.yarnpkg.com\",\"repo1.maven.org\",\"repository.apache.org\",\"s.symcb.com\",\"s.symcd.com\",\"scans-in.gradle.com\",\"search.maven.org\",\"security.ubuntu.com\",\"services.gradle.org\",\"skimdb.npmjs.com\",\"storage.googleapis.com\",\"telemetry.vercel.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\",\"www.java.com\",\"www.npmjs.com\",\"www.npmjs.org\",\"yarnpkg.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.10,squid=sha256:c06076f7aca95df713e0748c44d80c0a3c2538fad67bfdd04296d45158e083e6,agent=sha256:c01e6d16d11ea4f2a46cc023a9f402224a3b3861b026818eec0dc586d7e6918e,api-proxy=sha256:c3a18aebb8251339117ea998296315de17bada366f8d03919b3348ea71112e64,cli-proxy=sha256:a61070cb7f21840c5f2ec74d55b49adf0652d0348ce059015aaaca33a8cb6b45\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -923,8 +1008,13 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE @@ -937,8 +1027,8 @@ jobs: GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.85.4 + GH_AW_TIMEOUT_MINUTES: ${{ fromJSON(vars.GH_AW_DEFAULT_TIMEOUT_MINUTES || '20') }} + GH_AW_VERSION: v0.87.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -958,7 +1048,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: ${{ fromJSON(vars.GH_AW_DEFAULT_TIMEOUT_MINUTES || '20') }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -974,7 +1075,7 @@ jobs: continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -983,9 +1084,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -1009,15 +1112,17 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,adoptium.net,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.osv.dev,api.scorecard.dev,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,develocity.apache.org,dl.google.com,dlcdn.apache.org,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,ge.spockframework.org,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,host.docker.internal,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,search.maven.org,security.ubuntu.com,services.gradle.org,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,adoptium.net,api.adoptium.net,api.foojay.io,api.npms.io,api.osv.dev,api.scorecard.dev,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,develocity.apache.org,dl.google.com,dlcdn.apache.org,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,ge.spockframework.org,get.pnpm.io,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,search.maven.org,security.ubuntu.com,services.gradle.org,skimdb.npmjs.com,storage.googleapis.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_COMMANDS: "[\"supply-chain-review\"]" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -1027,9 +1132,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -1037,9 +1144,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -1053,9 +1162,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1063,16 +1174,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1089,6 +1225,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1128,7 +1266,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1137,8 +1275,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Supply Chain Review" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/supply-chain-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.78" - GH_AW_INFO_AWF_VERSION: "v0.27.44" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.10" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact @@ -1146,7 +1284,8 @@ jobs: continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1154,14 +1293,24 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() @@ -1180,6 +1329,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1202,9 +1353,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1231,8 +1384,8 @@ jobs: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: "1" GH_AW_WORKFLOW_NAME: "Supply Chain Review" - GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f/.github/workflows/supply-chain-review.md" + GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@e35840d877477896b1f0aa05d05371cb3b31ce9f" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/e35840d877477896b1f0aa05d05371cb3b31ce9f/.github/workflows/supply-chain-review.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_NOOP_REPORT_AS_ISSUE: "false" @@ -1243,9 +1396,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1253,17 +1408,19 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Supply Chain Review" - GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f/.github/workflows/supply-chain-review.md" + GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@e35840d877477896b1f0aa05d05371cb3b31ce9f" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/e35840d877477896b1f0aa05d05371cb3b31ce9f/.github/workflows/supply-chain-review.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1272,14 +1429,16 @@ jobs: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" GH_AW_WORKFLOW_NAME: "Supply Chain Review" - GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f/.github/workflows/supply-chain-review.md" + GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@e35840d877477896b1f0aa05d05371cb3b31ce9f" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/e35840d877477896b1f0aa05d05371cb3b31ce9f/.github/workflows/supply-chain-review.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1288,14 +1447,16 @@ jobs: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" GH_AW_WORKFLOW_NAME: "Supply Chain Review" - GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f/.github/workflows/supply-chain-review.md" + GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@e35840d877477896b1f0aa05d05371cb3b31ce9f" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/e35840d877477896b1f0aa05d05371cb3b31ce9f/.github/workflows/supply-chain-review.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1304,8 +1465,8 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Supply Chain Review" - GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f/.github/workflows/supply-chain-review.md" + GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@e35840d877477896b1f0aa05d05371cb3b31ce9f" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/e35840d877477896b1f0aa05d05371cb3b31ce9f/.github/workflows/supply-chain-review.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "supply-chain-review" @@ -1328,6 +1489,7 @@ jobs: GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1339,13 +1501,15 @@ jobs: GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" - GH_AW_TIMEOUT_MINUTES: "20" + GH_AW_TIMEOUT_MINUTES: "${{ fromJSON(vars.GH_AW_DEFAULT_TIMEOUT_MINUTES || '20') }}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); await main(); - name: Report failed jobs id: report_failed_jobs @@ -1354,16 +1518,18 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Supply Chain Review" - GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f/.github/workflows/supply-chain-review.md" + GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@e35840d877477896b1f0aa05d05371cb3b31ce9f" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/e35840d877477896b1f0aa05d05371cb3b31ce9f/.github/workflows/supply-chain-review.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_REPORT_FAILED_JOBS: "true" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_failed_jobs.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); - name: Update reaction comment with completion status id: conclusion @@ -1381,9 +1547,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/notify_comment_error.cjs'); + const { main } = require(path.join(actionsDir, 'notify_comment_error.cjs')); await main(); detection: @@ -1394,6 +1562,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1404,7 +1573,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1413,16 +1582,23 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Supply Chain Review" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/supply-chain-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.78" - GH_AW_INFO_AWF_VERSION: "v0.27.44" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.10" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1430,7 +1606,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -1442,7 +1620,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7 ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.10@sha256:c01e6d16d11ea4f2a46cc023a9f402224a3b3861b026818eec0dc586d7e6918e ghcr.io/github/gh-aw-firewall/api-proxy:0.28.10@sha256:c3a18aebb8251339117ea998296315de17bada366f8d03919b3348ea71112e64 ghcr.io/github/gh-aw-firewall/squid:0.28.10@sha256:c06076f7aca95df713e0748c44d80c0a3c2538fad67bfdd04296d45158e083e6 - name: Check if detection needed id: detection_guard if: always() @@ -1466,25 +1644,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - if [ -f "$f" ]; then - cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - fi - done - for f in /tmp/gh-aw/aw-*.bundle; do - if [ -f "$f" ]; then - cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - fi - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1493,85 +1653,51 @@ jobs: WORKFLOW_DESCRIPTION: "No description provided" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs')); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - rm -f /tmp/gh-aw/threat-detection/step-summary.md - touch /tmp/gh-aw/threat-detection/step-summary.md - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.10 --rootless - name: Install GitHub Copilot CLI run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - GH_AW_COMPILED_VERSION: v0.85.4 - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.44 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.87.10 + - name: Install threat-detect binary if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.44/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.44,squid=sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627,agent=sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4,api-proxy=sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7,cli-proxy=sha256:c064d15974f7c933ec7d3f7b4038f4fd203547b3154bdc821afd379144887eff\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - fi - # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} COPILOT_MODEL: gpt-5-mini + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.85.4 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.87.10 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1586,6 +1712,77 @@ jobs: GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} + WORKFLOW_NAME: "Supply Chain Review" + WORKFLOW_DESCRIPTION: "No description provided" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.10/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.10,squid=sha256:c06076f7aca95df713e0748c44d80c0a3c2538fad67bfdd04296d45158e083e6,agent=sha256:c01e6d16d11ea4f2a46cc023a9f402224a3b3861b026818eec0dc586d7e6918e,api-proxy=sha256:c3a18aebb8251339117ea998296315de17bada366f8d03919b3348ea71112e64,cli-proxy=sha256:a61070cb7f21840c5f2ec74d55b49adf0652d0348ce059015aaaca33a8cb6b45\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + fi + # shellcheck disable=SC1003,SC2016,SC2086 + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); + await main(); + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ + if-no-files-found: ignore - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() @@ -1595,49 +1792,22 @@ jobs: GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - - name: Upload threat detection log - if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: detection - path: /tmp/gh-aw/threat-detection/detection.log - if-no-files-found: ignore - - name: Parse and conclude threat detection + - name: Conclude threat detection id: detection_conclusion if: always() continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json pre_activation: if: "(github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' || contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)) && (github.event_name == 'issues' && (startsWith(github.event.issue.body, '/supply-chain-review ') || startsWith(github.event.issue.body, '/supply-chain-review\n') || github.event.issue.body == '/supply-chain-review') || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/supply-chain-review ') || startsWith(github.event.comment.body, '/supply-chain-review\n') || github.event.comment.body == '/supply-chain-review') && github.event.issue.pull_request == null || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/supply-chain-review ') || startsWith(github.event.comment.body, '/supply-chain-review\n') || github.event.comment.body == '/supply-chain-review') && github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/supply-chain-review ') || startsWith(github.event.comment.body, '/supply-chain-review\n') || github.event.comment.body == '/supply-chain-review') || github.event_name == 'pull_request' && (startsWith(github.event.pull_request.body, '/supply-chain-review ') || startsWith(github.event.pull_request.body, '/supply-chain-review\n') || github.event.pull_request.body == '/supply-chain-review') || github.event_name == 'discussion' && (startsWith(github.event.discussion.body, '/supply-chain-review ') || startsWith(github.event.discussion.body, '/supply-chain-review\n') || github.event.discussion.body == '/supply-chain-review') || github.event_name == 'discussion_comment' && (startsWith(github.event.comment.body, '/supply-chain-review ') || startsWith(github.event.comment.body, '/supply-chain-review\n') || github.event.comment.body == '/supply-chain-review'))" @@ -1653,15 +1823,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: GH_AW_SETUP_WORKFLOW_NAME: "Supply Chain Review" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/supply-chain-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.78" - GH_AW_INFO_AWF_VERSION: "v0.27.44" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.10" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check command position @@ -1671,9 +1841,11 @@ jobs: GH_AW_COMMANDS: "[\"supply-chain-review\"]" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_command_position.cjs'); + const { main } = require(path.join(actionsDir, 'check_command_position.cjs')); await main(); - name: Check team membership for command workflow id: check_membership @@ -1684,9 +1856,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); + const { main } = require(path.join(actionsDir, 'check_membership.cjs')); await main(); safe_outputs: @@ -1715,8 +1889,8 @@ jobs: GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "supply-chain-review" GH_AW_WORKFLOW_NAME: "Supply Chain Review" - GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f/.github/workflows/supply-chain-review.md" + GH_AW_WORKFLOW_SOURCE: "Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@e35840d877477896b1f0aa05d05371cb3b31ce9f" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/Alfresco/alfresco-build-tools/blob/e35840d877477896b1f0aa05d05371cb3b31ce9f/.github/workflows/supply-chain-review.md" outputs: code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} @@ -1724,15 +1898,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1741,16 +1920,19 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Supply Chain Review" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/supply-chain-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.78" - GH_AW_INFO_AWF_VERSION: "v0.27.44" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.10" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1758,7 +1940,9 @@ jobs: run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1774,16 +1958,18 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,adoptium.net,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.osv.dev,api.scorecard.dev,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,develocity.apache.org,dl.google.com,dlcdn.apache.org,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,ge.spockframework.org,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,host.docker.internal,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,search.maven.org,security.ubuntu.com,services.gradle.org,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,adoptium.net,api.adoptium.net,api.foojay.io,api.npms.io,api.osv.dev,api.scorecard.dev,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,develocity.apache.org,dl.google.com,dlcdn.apache.org,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,ge.spockframework.org,get.pnpm.io,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,search.maven.org,security.ubuntu.com,services.gradle.org,skimdb.npmjs.com,storage.googleapis.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"security:low\",\"security:medium\",\"security:high\"]},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"remove_labels\":{\"allowed\":[\"security:low\",\"security:medium\",\"security:high\"]},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"security:low\",\"security:medium\",\"security:high\"],\"issue_intent\":false},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"remove_labels\":{\"allowed\":[\"security:low\",\"security:medium\",\"security:high\"]},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/process_safe_outputs.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1793,4 +1979,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/supply-chain-review.md b/.github/workflows/supply-chain-review.md index 74a2e90d58..42255e8a89 100644 --- a/.github/workflows/supply-chain-review.md +++ b/.github/workflows/supply-chain-review.md @@ -8,10 +8,9 @@ permissions: contents: read pull-requests: read +model: gpt-5-mini engine: id: copilot - model: gpt-5-mini - tools: github: toolsets: [context, pull_requests, repos] @@ -30,11 +29,12 @@ safe-outputs: hide-older-comments: true add-labels: allowed: [security:low, security:medium, security:high] + issue-intent: false remove-labels: allowed: [security:low, security:medium, security:high] submit-pull-request-review: -source: Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@7bc0fc6f4f11df6c065b57d4a6aa90d7ea362b2f +source: Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@e35840d877477896b1f0aa05d05371cb3b31ce9f --- # Supply Chain Review diff --git a/.github/workflows/unit-test-workflow.yml b/.github/workflows/unit-test-workflow.yml index 2b52941aec..9de5b882cd 100644 --- a/.github/workflows/unit-test-workflow.yml +++ b/.github/workflows/unit-test-workflow.yml @@ -91,7 +91,7 @@ jobs: xvfb-run --auto-servernum pnpm nx run ${{ matrix.project }}:test - name: Upload coverage report if: ${{ always() }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-${{ matrix.project }} path: coverage/${{ matrix.project }}/lcov.info @@ -117,7 +117,7 @@ jobs: with: fetch-depth: 0 - name: Download all coverage artifacts - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: coverage-* path: coverage-reports @@ -138,7 +138,7 @@ jobs: echo "Coverage files found:" find coverage -name 'lcov.info' -type f - name: SonarCloud Scan - uses: SonarSource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0 + uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: https://sonarcloud.io diff --git a/docs/process-services-cloud/components/process-filters-cloud.component.md b/docs/process-services-cloud/components/process-filters-cloud.component.md index d960153d1d..31b3b4a6fe 100644 --- a/docs/process-services-cloud/components/process-filters-cloud.component.md +++ b/docs/process-services-cloud/components/process-filters-cloud.component.md @@ -27,6 +27,7 @@ Lists all available process filters and allows to select a filter. | appName | `string` | "" | (required) The application name | | filterParam | `UserTaskFilterRepresentation` | | (optional) The filter to be selected by default | | showIcons | `boolean` | false | (optional) Toggles showing an icon by the side of each filter | +| useBatchedCounters | `boolean` | false | Get all the filter counters with one call to `POST /query/v1/count` (needs Activiti 8.7.0). Turn it on for both filter components. | ### Events diff --git a/docs/process-services-cloud/components/task-filters-cloud.component.md b/docs/process-services-cloud/components/task-filters-cloud.component.md index d7580e5dea..acc85e1a68 100644 --- a/docs/process-services-cloud/components/task-filters-cloud.component.md +++ b/docs/process-services-cloud/components/task-filters-cloud.component.md @@ -36,6 +36,7 @@ Shows all available filters. | appName | `string` | "" | Display filters available to the current user for the application with the specified name. | | filterParam | `FilterParamsModel` | | Parameters to use for the task filter cloud. If there is no match then the default filter (the first one in the list) is selected. | | showIcons | `boolean` | false | Toggles display of the filter's icons. | +| useBatchedCounters | `boolean` | false | Get all the filter counters with one call to `POST /query/v1/count` (needs Activiti 8.7.0). Turn it on for both filter components. | ### Events diff --git a/lib/core/feature-flags/src/lib/interfaces/features.interface.ts b/lib/core/feature-flags/src/lib/interfaces/features.interface.ts index 6ec4d92e7b..7676cce16e 100644 --- a/lib/core/feature-flags/src/lib/interfaces/features.interface.ts +++ b/lib/core/feature-flags/src/lib/interfaces/features.interface.ts @@ -32,11 +32,13 @@ export interface QaFeaturesHelperConfig { helperExposeKeyOnDocument?: string; } +export interface FlagChangesetValues { + current: any; + previous: any; +} + export interface FlagChangeset { - [key: string]: { - current: any; - previous: any; - }; + [key: string]: FlagChangesetValues; } export interface WritableFlagChangeset { diff --git a/lib/core/feature-flags/src/lib/mocks/features-service-mock.factory.spec.ts b/lib/core/feature-flags/src/lib/mocks/features-service-mock.factory.spec.ts new file mode 100644 index 0000000000..6dc6f88d1d --- /dev/null +++ b/lib/core/feature-flags/src/lib/mocks/features-service-mock.factory.spec.ts @@ -0,0 +1,133 @@ +/*! + * @license + * Copyright Š 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { BehaviorSubject, firstValueFrom, of, Subject } from 'rxjs'; +import { FeaturesServiceToken, IFeaturesService } from '../interfaces/features.interface'; +import { MockFeatureFlags, provideMockFeatureFlags } from './features-service-mock.factory'; + +describe('provideMockFeatureFlags', () => { + let featureValue$: BehaviorSubject; + const feature1 = 'feature1'; + + const setupFeatureService = (featureFlags: MockFeatureFlags | string | string[]): IFeaturesService => { + TestBed.configureTestingModule({ + providers: [provideMockFeatureFlags(featureFlags)] + }); + + return TestBed.inject(FeaturesServiceToken); + }; + + beforeEach(() => { + featureValue$ = new BehaviorSubject(false); + }); + + it('should emit the updated value when an observable feature flag changes', async () => { + const featuresService = setupFeatureService({ [feature1]: featureValue$ }); + + expect(await firstValueFrom(featuresService.isOn$(feature1))).toBe(false); + expect(await firstValueFrom(featuresService.isOff$(feature1))).toBe(true); + + featureValue$.next(true); + + expect(await firstValueFrom(featuresService.isOn$(feature1))).toBe(true); + expect(await firstValueFrom(featuresService.isOff$(feature1))).toBe(false); + + featureValue$.next(false); + + expect(await firstValueFrom(featuresService.isOn$(feature1))).toBe(false); + expect(await firstValueFrom(featuresService.isOff$(feature1))).toBe(true); + }); + + it('should not let a consumer change the feature flag value through the observable returned by isOn$', async () => { + const originalValue$ = new BehaviorSubject(true); + const service = setupFeatureService({ [feature1]: originalValue$ }); + + const isOn$ = service.isOn$(feature1) as Subject; + + expect(isOn$).not.toBe(originalValue$); + expect(() => isOn$.next(false)).toThrow(); + expect(await firstValueFrom(service.isOn$(feature1))).toBe(true); + expect(await firstValueFrom(originalValue$)).toBe(true); + }); + + it('should mock a string feature flag as enabled', async () => { + const service = setupFeatureService(feature1); + + expect(await firstValueFrom(service.isOn$(feature1))).toBe(true); + expect(await firstValueFrom(service.isOff$(feature1))).toBe(false); + }); + + it('should mock every feature flag in an array as enabled', async () => { + const feature2 = 'feature2'; + const service = setupFeatureService([feature1, feature2]); + + expect(await firstValueFrom(service.getFlags$())).toEqual({ + [feature1]: { current: true, previous: null }, + [feature2]: { current: true, previous: null } + }); + }); + + it('should emit an empty changeset when no feature flags are provided', async () => { + const service = setupFeatureService({}); + + expect(await firstValueFrom(service.getFlags$())).toEqual({}); + expect(await firstValueFrom(service.init())).toEqual({}); + }); + + it('should emit an empty changeset when an empty feature flag array is provided', async () => { + const service = setupFeatureService([]); + + expect(await firstValueFrom(service.getFlags$())).toEqual({}); + expect(await firstValueFrom(service.init())).toEqual({}); + }); + + it('should return the expected state when the feature flag is true', async () => { + const service = setupFeatureService({ [feature1]: true }); + + expect(await firstValueFrom(service.init())).toEqual({ + [feature1]: { current: true, previous: null } + }); + expect(await firstValueFrom(service.isOn$(feature1))).toBe(true); + expect(await firstValueFrom(service.isOff$(feature1))).toBe(false); + }); + + it('should return the expected state when the feature flag is false', async () => { + const service = setupFeatureService({ [feature1]: false }); + + expect(await firstValueFrom(service.init())).toEqual({ + [feature1]: { current: false, previous: null } + }); + expect(await firstValueFrom(service.isOn$(feature1))).toBe(false); + expect(await firstValueFrom(service.isOff$(feature1))).toBe(true); + }); + + it('should resolve observable values in the complete feature flags result', async () => { + const service = setupFeatureService({ [feature1]: of(true) }); + + expect(await firstValueFrom(service.getFlags$())).toEqual({ + [feature1]: { current: true, previous: null } + }); + }); + + it('should throw when a feature flag has not been mocked', () => { + const featuresService = setupFeatureService({ [feature1]: true }); + + expect(() => featuresService.isOn$('missing-feature')).toThrowError(/missing-feature.*not mocked/); + expect(() => featuresService.isOff$('missing-feature')).toThrowError(/missing-feature.*not mocked/); + }); +}); diff --git a/lib/core/feature-flags/src/lib/mocks/features-service-mock.factory.ts b/lib/core/feature-flags/src/lib/mocks/features-service-mock.factory.ts index ab36f26f26..fca375ae61 100644 --- a/lib/core/feature-flags/src/lib/mocks/features-service-mock.factory.ts +++ b/lib/core/feature-flags/src/lib/mocks/features-service-mock.factory.ts @@ -15,11 +15,27 @@ * limitations under the License. */ -import { of } from 'rxjs'; -import { FeaturesServiceToken, FlagChangeset, IFeaturesService } from '../interfaces/features.interface'; +import { of, Observable, map, combineLatest, take, defer } from 'rxjs'; +import { FeaturesServiceToken, FlagChangeset, FlagChangesetValues, IFeaturesService } from '../interfaces/features.interface'; +/** + * Feature flags to mock. A boolean sets a fixed value, an observable lets the test change the value over time. + * + * Observable flags must have a current value, use 'BehaviorSubject' or 'of(...)'. + * A bare 'Subject' has no value until it emits, and 'getFlags$()'/'init()' withhold the whole changeset + * until every observable flag has emitted at least once. + */ export interface MockFeatureFlags { - [key: string]: boolean; + [key: string]: boolean | Observable; +} + +interface MockFlagChangesetValues extends FlagChangesetValues { + current: boolean | Observable; + previous: null; +} + +interface MockFlagChangeset extends FlagChangeset { + [key: string]: MockFlagChangesetValues; } const assertFeatureFlag = (flagChangeset: FlagChangeset, key: string): void => { @@ -32,19 +48,72 @@ const assertFeatureFlag = (flagChangeset: FlagChangeset, key: string): void => { } }; -const mockFeaturesService = (flagChangeset: FlagChangeset): IFeaturesService => ({ - init: () => of(flagChangeset), +/** + * Calling 'pipe' on a 'Subject' returns an 'AnonymousSubject' that still writes through to the original 'Subject', + * so piping alone does not stop a consumer from pushing values into the mocked flag. + * 'defer' breaks that chain and gives back a plain, read only observable. + * + * @param value$ Observable feature flag value provided by the test + * @returns Observable that cannot be used to change the mocked value + */ +const toReadOnly = (value$: Observable): Observable => defer(() => value$); + +const mockFeaturesService = (flagChangeset: MockFlagChangeset): IFeaturesService => ({ + init: () => resolveFeatureFlagValues(flagChangeset).pipe(take(1)), isOn$: (key) => { assertFeatureFlag(flagChangeset, key); - return of(flagChangeset[key].current); + const featureFlagValue = flagChangeset[key].current; + + // In case of an observable, we do not want to return the original observable, so a consumer cannot 'next', 'error' or 'complete' it + return typeof featureFlagValue === 'boolean' ? of(featureFlagValue) : toReadOnly(featureFlagValue).pipe(map(Boolean)); }, isOff$: (key) => { assertFeatureFlag(flagChangeset, key); - return of(!flagChangeset[key].current); + const featureFlagValue = flagChangeset[key].current; + + return typeof featureFlagValue === 'boolean' ? of(!featureFlagValue) : toReadOnly(featureFlagValue).pipe(map((value) => !value)); }, - getFlags$: () => of(flagChangeset) + getFlags$: () => resolveFeatureFlagValues(flagChangeset) }); +/** + * 'provideMockFeatureFlags' can receive observables, therefore we need to resolve these values + * + * @param mockFlagChangeset Mocked flag changeset + * @returns FlagChangeset + */ +const resolveFeatureFlagValues = (mockFlagChangeset: MockFlagChangeset): Observable => { + // No FF provided, just return empty object + if (Object.keys(mockFlagChangeset).length === 0) { + return of({}); + } + + const resolveFeatureFlagValues$ = Object.entries(mockFlagChangeset).map(([featureKey, values]) => { + if (typeof values.current === 'boolean') { + return of([featureKey, { ...values }] as const); + } + + // Value is observable, we need to resolve it + const observableValue$ = values.current; + + const resolveFlagValue$ = observableValue$.pipe(map((resolvedValue) => [featureKey, { current: resolvedValue, previous: null }] as const)); + + return resolveFlagValue$; + }); + + return combineLatest(resolveFeatureFlagValues$).pipe( + map((resolvedFlags) => { + const resolvedFeatureFlag: FlagChangeset = {}; + + resolvedFlags.forEach(([key, values]) => { + resolvedFeatureFlag[key] = values; + }); + + return resolvedFeatureFlag; + }) + ); +}; + const arrayToFlagChangeset = (featureFlags: string[]): FlagChangeset => { const flagChangeset: FlagChangeset = {}; featureFlags.forEach((featureFlag) => { @@ -62,6 +131,26 @@ const mockFeatureFlagsToFlagChangeset = (mockFeatureFlags: MockFeatureFlags) => return flagChangeset; }; +/** + * Mock the FeaturesService with the provided feature flags. + * A string or string[] sets every listed feature to true, a MockFeatureFlags object sets each value explicitly. + * Every flag the code under test asks for has to be mocked, otherwise 'isOn$'/'isOff$' throw. + * + * Use a 'BehaviorSubject' for a flag that changes during the test. A bare 'Subject' has no current value, + * so the flag stays silent and 'getFlags$()'/'init()' withhold the whole changeset, until it emits. + * + * @example + * + * const featureA$ = new BehaviorSubject(false); + * + * providers: [provideMockFeatureFlags('featureA')] + * providers: [provideMockFeatureFlags(['featureA', 'featureB'])] + * providers: [provideMockFeatureFlags({ featureA: true, featureB: false })] + * providers: [provideMockFeatureFlags({ featureA: featureA$ })] + * + * @param featureFlag The feature flag(s) to mock. Can be a single feature flag string, an array of feature flag strings, or a MockFeatureFlags object. + * @returns A provider object for the FeaturesServiceToken with the mocked feature flags. + */ export const provideMockFeatureFlags = (featureFlag: MockFeatureFlags | string | string[]) => { if (typeof featureFlag === 'string') { featureFlag = [featureFlag]; diff --git a/lib/core/src/lib/assets/images/ft_ic_file_link.svg b/lib/core/src/lib/assets/images/ft_ic_file_link.svg index b314a65c7e..833fa7fd88 100644 --- a/lib/core/src/lib/assets/images/ft_ic_file_link.svg +++ b/lib/core/src/lib/assets/images/ft_ic_file_link.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/lib/core/src/lib/assets/images/ft_ic_folder.svg b/lib/core/src/lib/assets/images/ft_ic_folder.svg index f176c89dd4..40c8d21ee2 100755 --- a/lib/core/src/lib/assets/images/ft_ic_folder.svg +++ b/lib/core/src/lib/assets/images/ft_ic_folder.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/lib/core/src/lib/assets/images/ft_ic_folder_empty.svg b/lib/core/src/lib/assets/images/ft_ic_folder_empty.svg index 811a91561a..46b965e165 100755 --- a/lib/core/src/lib/assets/images/ft_ic_folder_empty.svg +++ b/lib/core/src/lib/assets/images/ft_ic_folder_empty.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/lib/core/src/lib/assets/images/ft_ic_folder_rule.svg b/lib/core/src/lib/assets/images/ft_ic_folder_rule.svg index 8ec2b148cb..c0cada8b63 100644 --- a/lib/core/src/lib/assets/images/ft_ic_folder_rule.svg +++ b/lib/core/src/lib/assets/images/ft_ic_folder_rule.svg @@ -1 +1 @@ -folder-with-rule copyCreated with Sketch. \ No newline at end of file +folder-with-rule copyCreated with Sketch. \ No newline at end of file diff --git a/lib/core/src/lib/assets/images/ft_ic_folder_shortcut_link.svg b/lib/core/src/lib/assets/images/ft_ic_folder_shortcut_link.svg index 9a4ab93e91..0397e461a8 100644 --- a/lib/core/src/lib/assets/images/ft_ic_folder_shortcut_link.svg +++ b/lib/core/src/lib/assets/images/ft_ic_folder_shortcut_link.svg @@ -1 +1 @@ -folder-shortcut-link copyCreated with Sketch. \ No newline at end of file +folder-shortcut-link copyCreated with Sketch. \ No newline at end of file diff --git a/lib/core/src/lib/assets/images/ft_ic_miscellaneous.svg b/lib/core/src/lib/assets/images/ft_ic_miscellaneous.svg index ab1381ec45..fe57eeaae4 100755 --- a/lib/core/src/lib/assets/images/ft_ic_miscellaneous.svg +++ b/lib/core/src/lib/assets/images/ft_ic_miscellaneous.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/lib/core/src/lib/assets/images/ft_ic_smart_folder.svg b/lib/core/src/lib/assets/images/ft_ic_smart_folder.svg index 6c801c8c34..955a5d0d23 100644 --- a/lib/core/src/lib/assets/images/ft_ic_smart_folder.svg +++ b/lib/core/src/lib/assets/images/ft_ic_smart_folder.svg @@ -1 +1 @@ -filetype/folderCreated with Sketch. \ No newline at end of file +filetype/folderCreated with Sketch. \ No newline at end of file diff --git a/lib/core/src/lib/datatable/components/date-cell/date-cell.component.spec.ts b/lib/core/src/lib/datatable/components/date-cell/date-cell.component.spec.ts index 07410406db..e7d16fec66 100644 --- a/lib/core/src/lib/datatable/components/date-cell/date-cell.component.spec.ts +++ b/lib/core/src/lib/datatable/components/date-cell/date-cell.component.spec.ts @@ -67,19 +67,25 @@ const renderDateCell = (dateConfig: DateConfig, value: number | string | Date, t }; const checkDisplayedDate = (expectedDate: string) => { - const displayedDate = testingUtils.getByCSS('span').nativeElement.textContent.trim(); + const displayedDate = testingUtils.getByCSS('time').nativeElement.textContent.trim(); expect(displayedDate).toBeTruthy(); expect(displayedDate).toBe(expectedDate); }; const checkDisplayedTooltip = (expectedTooltip: string) => { - const displayedTooltip = testingUtils.getByCSS('span').nativeElement.title; + const displayedTooltip = testingUtils.getByCSS('time').nativeElement.title; expect(displayedTooltip).toBeTruthy(); expect(displayedTooltip).toBe(expectedTooltip); }; +const checkDatetimeAttribute = (expectedIso: string) => { + const datetime = testingUtils.getByCSS('time').nativeElement.getAttribute('datetime'); + + expect(datetime).toBe(expectedIso); +}; + const configureTestingModule = (providers: any[]) => { TestBed.configureTestingModule({ imports: [DateCellComponent], @@ -141,7 +147,7 @@ describe('DateCellComponent', () => { expect(component.config().locale).toEqual('en-US'); }); - it('should display date and tooltip with defaules values if NO dateConfig or appConfig is provided', () => { + it('should display date and tooltip with default values if NO dateConfig or appConfig is provided', () => { appConfigService.config = { dateValues: {} }; @@ -232,6 +238,16 @@ describe('DateCellComponent', () => { renderDateCell(mockDateConfig, mockTimestamp); checkDisplayedDate(expectedDate); }); + + it('should render a datetime attribute with the full ISO date value', () => { + const mockDateConfig: DateConfig = { + format: 'short', + tooltipFormat: 'short' + }; + + renderDateCell(mockDateConfig, mockDate); + checkDatetimeAttribute(mockDate.toISOString()); + }); }); describe('DateCellComponent locale', () => { diff --git a/lib/core/src/lib/datatable/components/date-cell/date-cell.component.ts b/lib/core/src/lib/datatable/components/date-cell/date-cell.component.ts index 9ba78d30f1..8c8460bcc1 100644 --- a/lib/core/src/lib/datatable/components/date-cell/date-cell.component.ts +++ b/lib/core/src/lib/datatable/components/date-cell/date-cell.component.ts @@ -26,7 +26,9 @@ import { toSignal } from '@angular/core/rxjs-interop'; selector: 'adf-date-cell', template: ` @if (formattedDate()) { - {{ formattedDate() }} + } `, encapsulation: ViewEncapsulation.None, @@ -55,6 +57,14 @@ export class DateCellComponent extends DataTableCellComponent implements OnInit // Convert value$ observable to signal for reactive computation private readonly dateValue = toSignal(this.value$); + // Computed signal that automatically formats the date to ISO string for datetime attribute + protected readonly isoDate = computed(() => { + const date = this.dateValue(); + const parsed = new Date(date); + + return isNaN(parsed.getTime()) ? null : parsed.toISOString(); + }); + // Computed signal that automatically formats the date based on value and config protected readonly formattedDate = computed(() => { const date = this.dateValue(); diff --git a/lib/core/src/lib/datatable/directives/drop-zone.directive.spec.ts b/lib/core/src/lib/datatable/directives/drop-zone.directive.spec.ts new file mode 100644 index 0000000000..66f3b58834 --- /dev/null +++ b/lib/core/src/lib/datatable/directives/drop-zone.directive.spec.ts @@ -0,0 +1,76 @@ +/*! + * @license + * Copyright Š 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { ElementRef } from '@angular/core'; +import { DropZoneDirective } from './drop-zone.directive'; + +describe('DropZoneDirective', () => { + let directive: DropZoneDirective; + let element: HTMLElement; + + beforeEach(() => { + element = document.createElement('div'); + + TestBed.configureTestingModule({ + providers: [{ provide: ElementRef, useValue: new ElementRef(element) }] + }); + + directive = TestBed.runInInjectionContext(() => new DropZoneDirective()); + directive.dropTarget = 'cell'; + directive.ngOnInit(); + }); + + it('should dispatch a namespaced custom event on dragenter while attached', () => { + const dispatched: string[] = []; + element.addEventListener('cell-dragenter', () => dispatched.push('cell-dragenter')); + + element.dispatchEvent(new DragEvent('dragenter')); + + expect(dispatched).toContain('cell-dragenter'); + }); + + it('should not handle drag events after the directive is destroyed', () => { + const dispatched: string[] = []; + element.addEventListener('cell-dragenter', () => dispatched.push('cell-dragenter')); + element.addEventListener('cell-dragover', () => dispatched.push('cell-dragover')); + element.addEventListener('cell-drop', () => dispatched.push('cell-drop')); + + directive.ngOnDestroy(); + + element.dispatchEvent(new DragEvent('dragenter')); + element.dispatchEvent(new DragEvent('dragover')); + element.dispatchEvent(new DragEvent('drop')); + + expect(dispatched).toEqual([]); + }); + + it('should remove listeners using the same references that were added', () => { + const addSpy = spyOn(element, 'addEventListener').and.callThrough(); + const removeSpy = spyOn(element, 'removeEventListener').and.callThrough(); + + directive.ngOnInit(); + directive.ngOnDestroy(); + + const addedByEvent = new Map(); + addSpy.calls.allArgs().forEach(([evt, fn]) => addedByEvent.set(evt as string, fn as EventListenerOrEventListenerObject)); + + removeSpy.calls.allArgs().forEach(([evt, fn]) => { + expect(fn).toBe(addedByEvent.get(evt as string)); + }); + }); +}); diff --git a/lib/core/src/lib/datatable/directives/drop-zone.directive.ts b/lib/core/src/lib/datatable/directives/drop-zone.directive.ts index 4d629db4ff..4da7c61e56 100644 --- a/lib/core/src/lib/datatable/directives/drop-zone.directive.ts +++ b/lib/core/src/lib/datatable/directives/drop-zone.directive.ts @@ -36,6 +36,10 @@ export class DropZoneDirective implements OnInit, OnDestroy { @Input() dropColumn: DataColumn; + private readonly onDragEnterHandler = this.onDragEnter.bind(this); + private readonly onDragOverHandler = this.onDragOver.bind(this); + private readonly onDropHandler = this.onDrop.bind(this); + constructor() { const elementRef = inject(ElementRef); @@ -44,16 +48,16 @@ export class DropZoneDirective implements OnInit, OnDestroy { ngOnInit() { this.ngZone.runOutsideAngular(() => { - this.element.addEventListener('dragenter', this.onDragEnter.bind(this)); - this.element.addEventListener('dragover', this.onDragOver.bind(this)); - this.element.addEventListener('drop', this.onDrop.bind(this)); + this.element.addEventListener('dragenter', this.onDragEnterHandler); + this.element.addEventListener('dragover', this.onDragOverHandler); + this.element.addEventListener('drop', this.onDropHandler); }); } ngOnDestroy() { - this.element.removeEventListener('dragenter', this.onDragEnter); - this.element.removeEventListener('dragover', this.onDragOver); - this.element.removeEventListener('drop', this.onDrop); + this.element.removeEventListener('dragenter', this.onDragEnterHandler); + this.element.removeEventListener('dragover', this.onDragOverHandler); + this.element.removeEventListener('drop', this.onDropHandler); } onDragEnter(event: DragEvent) { diff --git a/lib/core/src/lib/form/components/form-renderer.component.html b/lib/core/src/lib/form/components/form-renderer.component.html index 337ad73c58..d025b0e7e1 100644 --- a/lib/core/src/lib/form/components/form-renderer.component.html +++ b/lib/core/src/lib/form/components/form-renderer.component.html @@ -98,7 +98,7 @@ [hidden]="!currentRootElement?.isVisible" > - @for (row of currentRootElement.field.rows; track row; let rowIndex = $index) { + @for (row of currentRootElement.field.rows; track row.id; let rowIndex = $index) { @let hasMultipleRows = currentRootElement.field.rows.length > 1;
- @for (column of row.columns; track column; let columnIndex = $index) { + @for (column of row.columns; track column.id; let columnIndex = $index) {
- @for (field of column?.fields; track field) { + @for (field of column?.fields; track field.id) { @if (field.type === 'section') { } @else { diff --git a/lib/core/src/lib/form/components/form-renderer.component.scss b/lib/core/src/lib/form/components/form-renderer.component.scss index 956c09af4d..0e906f4d26 100644 --- a/lib/core/src/lib/form/components/form-renderer.component.scss +++ b/lib/core/src/lib/form/components/form-renderer.component.scss @@ -21,10 +21,10 @@ width: auto; } - .adf-form-field-input:not(.adf-inplace-input-mat-form-field, .adf-people-cloud, .adf-cloud-group) { - #{ms.$mat-form-field-subscript-wrapper} { - height: 40px; - } + .adf-form-field-status-slot { + display: block; + height: 40px; + box-sizing: border-box; } } @@ -33,6 +33,7 @@ .adf-form-tab-content { margin-top: 1em; + padding-bottom: 3px; } .adf-form-tab-group { diff --git a/lib/core/src/lib/form/components/widgets/amount/amount.widget.html b/lib/core/src/lib/form/components/widgets/amount/amount.widget.html index 00fefd8e34..76104bba6d 100644 --- a/lib/core/src/lib/form/components/widgets/amount/amount.widget.html +++ b/lib/core/src/lib/form/components/widgets/amount/amount.widget.html @@ -10,7 +10,11 @@ >
- + @if ( (field.name || field?.required) && !field.leftLabels) { {{field.name | translate }} } @if(!enableDisplayBasedOnLocale) { {{ currency }}  @@ -32,12 +36,13 @@ (blur)="amountWidgetOnBlur()" /> @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) { - + error_outline @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}} } +
diff --git a/lib/core/src/lib/form/components/widgets/core/form-variable.model.ts b/lib/core/src/lib/form/components/widgets/core/form-variable.model.ts index 0bac1bb08a..3efe973bbe 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-variable.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-variable.model.ts @@ -20,4 +20,5 @@ export interface FormVariableModel { name: string; type: string; value?: any; + runtimeSet?: boolean; } diff --git a/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts b/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts index f2a834bb54..90d8fcd047 100644 --- a/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts +++ b/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts @@ -600,6 +600,33 @@ describe('FormModel', () => { const missing = form.getProcessVariableValue('missing'); expect(missing).toBeUndefined(); }); + + it('should return zero process variable value instead of the form default', () => { + const formWithZero = new FormModel({ + variables: [{ id: 'amount-var', name: 'amount', type: 'integer', value: 99 }], + processVariables: [{ name: 'variables.amount', value: 0, type: 'integer' }] + }); + + expect(formWithZero.getProcessVariableValue('amount')).toBe(0); + }); + + it('should return false process variable value instead of the form default', () => { + const formWithFalse = new FormModel({ + variables: [{ id: 'flag-var', name: 'flag', type: 'boolean', value: true }], + processVariables: [{ name: 'variables.flag', value: false, type: 'boolean' }] + }); + + expect(formWithFalse.getProcessVariableValue('flag')).toBe(false); + }); + + it('should return empty string process variable value instead of the form default', () => { + const formWithEmpty = new FormModel({ + variables: [{ id: 'text-var', name: 'text', type: 'string', value: 'default' }], + processVariables: [{ name: 'variables.text', value: '', type: 'string' }] + }); + + expect(formWithEmpty.getProcessVariableValue('text')).toBe(''); + }); }); describe('add values not present', () => { diff --git a/lib/core/src/lib/form/components/widgets/core/form.model.ts b/lib/core/src/lib/form/components/widgets/core/form.model.ts index 2fea8ce8dd..1d53f2f2d1 100644 --- a/lib/core/src/lib/form/components/widgets/core/form.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form.model.ts @@ -307,22 +307,17 @@ export class FormModel implements ProcessFormModel { * @returns process variable value */ getProcessVariableValue(name: string): any { - let value; if (this.processVariables?.length) { const names = [`variables.${name}`, name]; const processVariable = this.processVariables.find((entry) => names.includes(entry.name)); if (processVariable) { - value = this.parseValue(processVariable.type, processVariable.value); + return this.parseValue(processVariable.type, processVariable.value); } } - if (!value) { - value = this.getDefaultFormVariableValue(name); - } - - return value; + return this.getDefaultFormVariableValue(name); } protected parseValue(type: string, value: any): any { @@ -535,9 +530,20 @@ export class FormModel implements ProcessFormModel { const variable = this.getFormVariable(variableId); if (variable) { variable.value = value; + variable.runtimeSet = true; } } + /** + * Checks whether a form variable has been given a value at runtime, for example by a form rule. + * + * @param identifier The `name` or `id` value + * @returns `true` when the value was set at runtime rather than coming from the form definition + */ + isVariableSetAtRuntime(identifier: string): boolean { + return !!this.getFormVariable(identifier)?.runtimeSet; + } + private loadInjectedFieldValidators(injectedFieldValidators: FormFieldValidator[]): void { this.fieldValidators = injectedFieldValidators ? [...FORM_FIELD_VALIDATORS, ...injectedFieldValidators] : [...FORM_FIELD_VALIDATORS]; } diff --git a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.html b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.html index d1c5a7b33b..1457221133 100644 --- a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.html +++ b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.html @@ -10,6 +10,7 @@
@if( (field.name || field?.required) && !field.leftLabels) { @@ -38,11 +39,12 @@ [timeInterval]="5" [disabled]="field.readOnly" /> @if (datetimeInputControl.invalid && datetimeInputControl.touched && field.validationSummary?.message) { - + error_outline {{ field.validationSummary.message | translate:translateParameters }} } +
diff --git a/lib/core/src/lib/form/components/widgets/date/date.widget.html b/lib/core/src/lib/form/components/widgets/date/date.widget.html index f36c6ae24e..8633b8db24 100644 --- a/lib/core/src/lib/form/components/widgets/date/date.widget.html +++ b/lib/core/src/lib/form/components/widgets/date/date.widget.html @@ -1,7 +1,7 @@
- + @@ -22,11 +22,12 @@ [startAt]="startAt" [disabled]="field.readOnly" /> @if (dateInputControl.invalid && dateInputControl.touched) { - + error_outline @if (dateInputControl.hasError('required')) {{{ 'FORM.FIELD.REQUIRED' | translate }}} @else if (dateInputControl.hasError('matDatepickerParse')) {{{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate: { format: field.dateDisplayFormat || field.defaultDateTimeFormat } }}} @else if (dateInputControl.hasError('matDatepickerMin')) {{{ 'FORM.FIELD.VALIDATOR.NOT_LESS_THAN' | translate: { minValue: formattedMinDate } }}} @else if (dateInputControl.hasError('matDatepickerMax')) {{{ 'FORM.FIELD.VALIDATOR.NOT_GREATER_THAN' | translate: { maxValue: formattedMaxDate } }}} } +
diff --git a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html index e2c5be4965..c531e78288 100644 --- a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html +++ b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html @@ -9,7 +9,7 @@
- + @if ( (field.name || field?.required) && !field.leftLabels) { {{ field.name | translate }} } @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) { - + error_outline @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}} } +
diff --git a/lib/core/src/lib/form/components/widgets/hyperlink/hyperlink.widget.scss b/lib/core/src/lib/form/components/widgets/hyperlink/hyperlink.widget.scss index 91025303f0..4dedad8795 100644 --- a/lib/core/src/lib/form/components/widgets/hyperlink/hyperlink.widget.scss +++ b/lib/core/src/lib/form/components/widgets/hyperlink/hyperlink.widget.scss @@ -1,7 +1,6 @@ .adf-hyperlink-widget { padding: 0.4375em 0; - border-top: 0.8438em solid transparent; - margin-bottom: 20px; + margin-bottom: 40px; a { color: var(--mat-sys-primary); diff --git a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.html b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.html index cea1673327..73d7d5036a 100644 --- a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.html +++ b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.html @@ -7,6 +7,7 @@ @@ -31,14 +32,16 @@ > @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) { - + @if (field.maxLength > 0) {{{ field?.value?.length || 0 }}/{{ field.maxLength }}} error_outline @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}} } @else if (field.maxLength > 0) { - {{ field?.value?.length || 0 }}/{{ field.maxLength }} + {{ field?.value?.length || 0 }}/{{ field.maxLength }} + } @else { + diff --git a/lib/core/src/lib/form/components/widgets/number/number.widget.html b/lib/core/src/lib/form/components/widgets/number/number.widget.html index 11999ea2b1..344d5c9abd 100644 --- a/lib/core/src/lib/form/components/widgets/number/number.widget.html +++ b/lib/core/src/lib/form/components/widgets/number/number.widget.html @@ -9,7 +9,7 @@
- + @if( (field.name || this.field?.required) && !field.leftLabels) { {{ field.name | translate }} @@ -30,12 +30,13 @@ [errorStateMatcher]="errorStateMatcher" (blur)="onBlur()"> @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) { - + error_outline @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}} } +
diff --git a/lib/core/src/lib/form/components/widgets/text/text.widget.html b/lib/core/src/lib/form/components/widgets/text/text.widget.html index e1fc6d47e0..e9dee6e3b9 100644 --- a/lib/core/src/lib/form/components/widgets/text/text.widget.html +++ b/lib/core/src/lib/form/components/widgets/text/text.widget.html @@ -8,7 +8,11 @@
- + @if ( (field.name || this.field?.required) && !field.leftLabels) { {{ field.name | translate }} @@ -30,7 +34,7 @@ (paste)="onPaste($event)" (blur)="onBlur()"> @if (!fieldStatusTemplate && (maxLengthPasteError.isActive() || field.validationSummary?.message || (isInvalidFieldRequired() && isTouched()))) { - + @if (maxLengthPasteError.isActive()) { @@ -43,6 +47,9 @@ } + @if (!fieldStatusTemplate) { + diff --git a/lib/core/src/lib/form/models/task-process-variable.model.ts b/lib/core/src/lib/form/models/task-process-variable.model.ts index 1bfc188caa..f2f764f366 100644 --- a/lib/core/src/lib/form/models/task-process-variable.model.ts +++ b/lib/core/src/lib/form/models/task-process-variable.model.ts @@ -18,5 +18,5 @@ export class TaskProcessVariableModel { id?: string; type?: string; - value: string; + value: any; } diff --git a/lib/core/src/lib/form/services/widget-visibility.service.spec.ts b/lib/core/src/lib/form/services/widget-visibility.service.spec.ts index a75f9fa768..ce761cace6 100644 --- a/lib/core/src/lib/form/services/widget-visibility.service.spec.ts +++ b/lib/core/src/lib/form/services/widget-visibility.service.spec.ts @@ -1002,4 +1002,169 @@ describe('WidgetVisibilityService', () => { expect(textField.isVisible).toBe(true); }); }); + + describe('Visibility calculation from form variables', () => { + const hiddenWhileRequestor = new WidgetVisibilityModel({ + leftType: 'variable', + leftValue: 'person_type', + operator: '!=', + rightType: 'value', + rightValue: 'Requestor', + nextConditionOperator: '', + nextCondition: null + }); + + const requestorProcessVariables = [{ id: 'variables.person_type', value: 'Requestor' }]; + + let formJson: any; + let form: FormModel; + + beforeEach(() => { + formJson = { + id: 'person-type-form', + variables: [{ id: 'person-type-var', name: 'person_type', value: null }], + processVariables: [{ name: 'variables.person_type', value: 'Requestor' }] + }; + form = new FormModel(formJson); + service.cleanProcessVariable(); + }); + + it('should resolve a variable from the form when the cached process variables no longer hold it', () => { + service.refreshVisibility(form, requestorProcessVariables); + + service.refreshVisibility(new FormModel({ id: 'another-form' }), [{ id: 'variables.other', value: 'other' }]); + + expect(service.evaluateVisibility(form, hiddenWhileRequestor)).toBe(false); + }); + + it('should resolve a variable from the form when the refreshed data omits it', () => { + service.refreshVisibility(form, requestorProcessVariables); + service.refreshVisibility(form, [{ id: 'processOutput', value: 'result' }]); + + expect(service.evaluateVisibility(form, hiddenWhileRequestor)).toBe(false); + }); + + it('should prefer a process variable over the value defined in the form definition', () => { + const formWithDefault = new FormModel({ + id: 'person-type-form-with-default', + variables: [{ id: 'person-type-var', name: 'person_type', value: 'Approver' }], + processVariables: [{ name: 'variables.person_type', value: 'Requestor' }] + }); + + expect(service.getVariableValue(formWithDefault, 'person_type', requestorProcessVariables)).toBe('Requestor'); + }); + + it('should prefer a variable changed at runtime over a process variable of the same name', () => { + service.refreshVisibility(form, requestorProcessVariables); + + form.changeVariableValue('person-type-var', 'Approver'); + service.refreshVisibility(form, requestorProcessVariables); + + expect(service.evaluateVisibility(form, hiddenWhileRequestor)).toBe(true); + }); + + it('should prefer a variable cleared at runtime over a process variable of the same name', () => { + service.refreshVisibility(form, requestorProcessVariables); + + form.changeVariableValue('person-type-var', ''); + service.refreshVisibility(form, requestorProcessVariables); + + expect(service.evaluateVisibility(form, hiddenWhileRequestor)).toBe(true); + }); + + it('should keep a variable changed at runtime when the form is rebuilt from the same definition', () => { + service.refreshVisibility(form, requestorProcessVariables); + form.changeVariableValue('person-type-var', 'Approver'); + + const rebuiltForm = new FormModel(formJson); + service.refreshVisibility(rebuiltForm, [{ id: 'processOutput', value: 'result' }]); + + expect(service.evaluateVisibility(rebuiltForm, hiddenWhileRequestor)).toBe(true); + }); + + it('should not convert the type of a variable changed at runtime', () => { + const numericForm = new FormModel({ id: 'numeric-form', variables: [{ id: 'amount-var', name: 'amount', type: 'integer', value: 0 }] }); + const booleanForm = new FormModel({ id: 'boolean-form', variables: [{ id: 'flag-var', name: 'flag', type: 'boolean', value: true }] }); + + service.refreshVisibility(numericForm); + service.refreshVisibility(booleanForm); + + numericForm.changeVariableValue('amount-var', 5); + booleanForm.changeVariableValue('flag-var', false); + + service.refreshVisibility(numericForm); + service.refreshVisibility(booleanForm); + + expect(service.getVariableValue(numericForm, 'amount')).toBe(5); + expect(service.getVariableValue(booleanForm, 'flag', [])).toBe(false); + }); + + it('should evaluate a numeric variable changed at runtime without string comparison', () => { + const numericForm = new FormModel({ + id: 'numeric-visibility-form', + variables: [{ id: 'amount-var', name: 'amount', type: 'integer', value: 0 }] + }); + const amountOverTen = new WidgetVisibilityModel({ + leftType: 'variable', + leftValue: 'amount', + operator: '>', + rightType: 'value', + rightValue: '10', + nextConditionOperator: '', + nextCondition: null + }); + + service.refreshVisibility(numericForm); + numericForm.changeVariableValue('amount-var', 5); + service.refreshVisibility(numericForm); + + expect(service.evaluateVisibility(numericForm, amountOverTen)).toBe(false); + }); + + it('should resolve a zero process variable from the form when the refreshed data omits it', () => { + const zeroForm = new FormModel({ + id: 'zero-form', + variables: [{ id: 'amount-var', name: 'amount', type: 'integer', value: 99 }], + processVariables: [{ name: 'variables.amount', value: 0, type: 'integer' }] + }); + const amountIsZero = new WidgetVisibilityModel({ + leftType: 'variable', + leftValue: 'amount', + operator: '==', + rightType: 'value', + rightValue: '0', + nextConditionOperator: '', + nextCondition: null + }); + + service.refreshVisibility(zeroForm, [{ id: 'variables.amount', value: 0 }]); + service.refreshVisibility(zeroForm, [{ id: 'processOutput', value: 'result' }]); + + expect(service.getVariableValue(zeroForm, 'amount', [])).toBe(0); + expect(service.evaluateVisibility(zeroForm, amountIsZero)).toBe(true); + }); + + it('should resolve a false process variable from the form when the refreshed data omits it', () => { + const falseForm = new FormModel({ + id: 'false-form', + variables: [{ id: 'flag-var', name: 'flag', type: 'boolean', value: true }], + processVariables: [{ name: 'variables.flag', value: false, type: 'boolean' }] + }); + const flagIsFalse = new WidgetVisibilityModel({ + leftType: 'variable', + leftValue: 'flag', + operator: '==', + rightType: 'value', + rightValue: 'false', + nextConditionOperator: '', + nextCondition: null + }); + + service.refreshVisibility(falseForm, [{ id: 'variables.flag', value: false }]); + service.refreshVisibility(falseForm, [{ id: 'processOutput', value: 'result' }]); + + expect(service.getVariableValue(falseForm, 'flag', [])).toBe(false); + expect(service.evaluateVisibility(falseForm, flagIsFalse)).toBe(true); + }); + }); }); diff --git a/lib/core/src/lib/form/services/widget-visibility.service.ts b/lib/core/src/lib/form/services/widget-visibility.service.ts index a7d1ea039d..618a08d2e1 100644 --- a/lib/core/src/lib/form/services/widget-visibility.service.ts +++ b/lib/core/src/lib/form/services/widget-visibility.service.ts @@ -29,7 +29,7 @@ import { FormService } from './form.service'; export class WidgetVisibilityService { private readonly formService = inject(FormService); - private processVarList: TaskProcessVariableModel[]; + private processVarList: TaskProcessVariableModel[] = []; private form: FormModel; public refreshVisibility(form: FormModel, processVarList?: TaskProcessVariableModel[]) { @@ -110,7 +110,7 @@ export class WidgetVisibilityService { } } - getLeftValue(form: FormModel, visibilityObj: WidgetVisibilityModel): string { + getLeftValue(form: FormModel, visibilityObj: WidgetVisibilityModel): any { let leftValue = ''; if (visibilityObj.leftType === WidgetTypeEnum.variable) { leftValue = this.getVariableValue(form, visibilityObj.leftValue, this.processVarList); @@ -124,7 +124,7 @@ export class WidgetVisibilityService { return leftValue; } - getRightValue(form: FormModel, visibilityObj: WidgetVisibilityModel): string { + getRightValue(form: FormModel, visibilityObj: WidgetVisibilityModel): any { let valueFound = ''; if (visibilityObj.rightType === WidgetTypeEnum.variable) { @@ -270,20 +270,21 @@ export class WidgetVisibilityService { return field.id && fieldToFind ? field.id.toUpperCase() === fieldToFind.toUpperCase() : false; } - public getVariableValue(form: FormModel, name: string, processVarList: TaskProcessVariableModel[]): string { - const processVariableValue = this.getProcessVariableValue(name, processVarList); - const variableDefaultValue = form.getDefaultFormVariableValue(name); + public getVariableValue(form: FormModel, name: string, processVarList: TaskProcessVariableModel[] = []): any { + if (form.isVariableSetAtRuntime(name)) { + return form.getDefaultFormVariableValue(name); + } - return processVariableValue === undefined ? variableDefaultValue : processVariableValue; + const processVariableValue = this.getProcessVariableValue(name, processVarList); + + return processVariableValue === undefined ? form.getProcessVariableValue(name) : processVariableValue; } - private getProcessVariableValue(name: string, processVarList: TaskProcessVariableModel[]): string { - if (processVarList) { - const processVariable = processVarList.find((variable) => variable.id === name || variable.id === `variables.${name}`); + private getProcessVariableValue(name: string, processVarList: TaskProcessVariableModel[]): any { + const processVariable = processVarList.find((variable) => variable.id === name || variable.id === `variables.${name}`); - if (processVariable) { - return processVariable.value; - } + if (processVariable) { + return processVariable.value; } return undefined; } diff --git a/lib/core/src/lib/styles/_mat-selectors.scss b/lib/core/src/lib/styles/_mat-selectors.scss index dba941cafa..31fa2b1208 100644 --- a/lib/core/src/lib/styles/_mat-selectors.scss +++ b/lib/core/src/lib/styles/_mat-selectors.scss @@ -17,7 +17,6 @@ $mat-button: '.mat-mdc-button'; $mat-button-label: '.mdc-button__label'; $mat-form-field: '.mat-mdc-form-field'; $mat-form-field-wrapper: '.mat-mdc-text-field-wrapper'; -$mat-form-field-subscript-wrapper: '.mat-mdc-form-field-subscript-wrapper'; $mat-line-ripple: '.mdc-line-ripple'; $mat-form-field-prefix: '.mat-mdc-form-field-text-prefix'; $mat-form-field-suffix: '.mat-mdc-form-field-text-suffix'; diff --git a/lib/insights/package.json b/lib/insights/package.json index bf0225cff7..1571dd03d8 100644 --- a/lib/insights/package.json +++ b/lib/insights/package.json @@ -11,9 +11,9 @@ "url": "https://github.com/Alfresco/alfresco-ng2-components/issues" }, "dependencies": { - "chart.js": "^4.3.0", - "ng2-charts": "^4.1.1", - "raphael": ">=2.3.0" + "chart.js": "4.5.1", + "ng2-charts": "9.0.0", + "raphael": "2.3.0" }, "peerDependencies": { "@angular/common": ">=20.3.27", diff --git a/lib/insights/src/lib/analytics-process/components/analytics-generator.component.ts b/lib/insights/src/lib/analytics-process/components/analytics-generator.component.ts index 2679c765da..d84aff0d29 100644 --- a/lib/insights/src/lib/analytics-process/components/analytics-generator.component.ts +++ b/lib/insights/src/lib/analytics-process/components/analytics-generator.component.ts @@ -22,7 +22,7 @@ import { AnalyticsService } from '../services/analytics.service'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { NgChartsModule } from 'ng2-charts'; +import { BaseChartDirective } from 'ng2-charts'; import { TranslatePipe } from '@ngx-translate/core'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { FormsModule } from '@angular/forms'; @@ -34,7 +34,7 @@ import { AnalyticsReportHeatMapComponent } from './analytics-report-heat-map.com CommonModule, MatButtonModule, MatIconModule, - NgChartsModule, + BaseChartDirective, TranslatePipe, MatCheckboxModule, FormsModule, diff --git a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts index 221f4dedb5..8467382568 100644 --- a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts @@ -2575,3 +2575,126 @@ describe('FormCloudComponent — runtime state preservation on data refresh', () }); }); }); + +describe('FormCloudComponent — form variable visibility on data refresh', () => { + let fixture: ComponentFixture; + let formComponent: FormCloudComponent; + let visibilityService: WidgetVisibilityService; + + /** field is hidden while person_type is Requestor */ + const personTypeFormJson = { + id: 'person-type-form', + name: 'Person Type Form', + variables: [{ id: 'person-type-var', name: 'person_type', value: null }], + fields: [ + { + fieldType: 'ContainerRepresentation', + id: 'container1', + name: 'Container', + type: 'container', + tab: null, + numberOfColumns: 1, + fields: { + 1: [ + { + fieldType: 'FormFieldRepresentation', + id: 'conditionalField', + name: 'Conditional Field', + type: 'multiline-text', + value: null, + required: false, + readOnly: false, + visibilityCondition: { + leftType: 'variable', + leftValue: 'person_type', + operator: '!=', + rightType: 'value', + rightValue: 'Requestor', + nextConditionOperator: '', + nextCondition: null + }, + params: { existingColspan: 1, maxColspan: 1 } + } + ] + } + } + ] + }; + + const requestorVariables = () => [new TaskVariableCloud({ name: 'variables.person_type', value: 'Requestor' })]; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NoopTranslateModule, NoopAuthModule, FormCloudComponent], + providers: [ + { provide: VersionCompatibilityService, useValue: {} }, + { provide: FormRenderingService, useClass: CloudFormRenderingService } + ] + }); + + const apiService = TestBed.inject(AlfrescoApiService); + spyOn(apiService, 'getInstance').and.returnValue(mockOauth2Auth); + + visibilityService = TestBed.inject(WidgetVisibilityService); + visibilityService.cleanProcessVariable(); + + fixture = TestBed.createComponent(FormCloudComponent); + formComponent = fixture.componentInstance; + + formComponent.formCloudRepresentationJSON = new FormCloudRepresentation(JSON.parse(JSON.stringify(personTypeFormJson))); + formComponent.formCloudRepresentationJSON.processVariables = requestorVariables(); + formComponent.data = requestorVariables(); + formComponent.form = formComponent.parseForm(formComponent.formCloudRepresentationJSON); + visibilityService.refreshVisibility(formComponent.form, formComponent.data); + + fixture.detectChanges(); + }); + + it('should keep the field hidden when the refreshed data omits the variable', () => { + expect(formComponent.form.getFieldById('conditionalField').isVisible).toBeFalse(); + + const partialData = [new TaskVariableCloud({ name: 'processOutput', value: 'result' })]; + const change = new SimpleChange(formComponent.data, partialData, false); + formComponent.data = partialData; + + formComponent.ngOnChanges({ data: change }); + + expect(formComponent.form.getFieldById('conditionalField').isVisible).toBeFalse(); + }); + + it('should show the field when the refreshed data changes the variable', () => { + const approverData = [new TaskVariableCloud({ name: 'variables.person_type', value: 'Approver' })]; + const change = new SimpleChange(formComponent.data, approverData, false); + formComponent.data = approverData; + + formComponent.ngOnChanges({ data: change }); + + expect(formComponent.form.getFieldById('conditionalField').isVisible).toBeTrue(); + }); + + it('should keep a variable changed by a form rule when the refreshed data omits it', () => { + formComponent.form.changeVariableValue('person-type-var', 'Approver'); + + const partialData = [new TaskVariableCloud({ name: 'processOutput', value: 'result' })]; + const change = new SimpleChange(formComponent.data, partialData, false); + formComponent.data = partialData; + + formComponent.ngOnChanges({ data: change }); + + expect(formComponent.form.getFieldById('conditionalField').isVisible).toBeTrue(); + }); + + it('should keep the latest received variable value across a following partial refresh', () => { + const approverData = [new TaskVariableCloud({ name: 'variables.person_type', value: 'Approver' })]; + formComponent.data = approverData; + formComponent.ngOnChanges({ data: new SimpleChange(requestorVariables(), approverData, false) }); + + expect(formComponent.form.getFieldById('conditionalField').isVisible).toBeTrue(); + + const partialData = [new TaskVariableCloud({ name: 'processOutput', value: 'result' })]; + formComponent.data = partialData; + formComponent.ngOnChanges({ data: new SimpleChange(approverData, partialData, false) }); + + expect(formComponent.form.getFieldById('conditionalField').isVisible).toBeTrue(); + }); +}); diff --git a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts index c2ab42da40..66f91cc602 100644 --- a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts @@ -601,6 +601,8 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, private refreshFormData(previousData: TaskVariableCloud[] = []) { const snapshot = this.snapshotRuntimeState(); + this.mergeProcessVariables(this.data ?? []); + this.form = this.parseForm(this.formCloudRepresentationJSON); if (!this.form) { return; @@ -610,13 +612,36 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, this.restoreRuntimeState(this.form, snapshot, changedFieldIds); this.setCheckParentVisibilityForValidationOnFields(); - this.visibilityService.refreshVisibility(this.form); + this.visibilityService.refreshVisibility(this.form, this.data); this.form.validateForm(); this.onFormLoaded(this.form); this.formService.formRulesEvent.next(new FormRulesEvent('dataRefreshed', new FormEvent(this.form))); this.onFormDataRefreshed(this.form); } + /** + * Keeps the process variables on the stored representation up to date with the latest data, so that a + * variable omitted by a later partial refresh still resolves to the most recent value received. + * + * @param updates Variables received on the latest data refresh + */ + private mergeProcessVariables(updates: TaskVariableCloud[]): void { + if (!this.formCloudRepresentationJSON) { + return; + } + + const existing: TaskVariableCloud[] = this.formCloudRepresentationJSON.processVariables ?? []; + const byName = new Map(); + + for (const variable of [...existing, ...updates]) { + if (variable?.name) { + byName.set(variable.name, variable); + } + } + + this.formCloudRepresentationJSON.processVariables = Array.from(byName.values()); + } + private snapshotRuntimeState(): Map { const snapshot = new Map(); if (!this.form) { diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.scss b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.scss index ed9398c207..8fcb2550ac 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.scss +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.scss @@ -1,11 +1,12 @@ .adf-data-table-widget-failed-message { display: block; - margin: 10px; } -.adf-preview-placeholder { - height: 100%; - width: 100%; - min-height: 100px; - margin-bottom: 10px; +.adf-data-table-widget-container { + .adf-preview-placeholder { + height: 100%; + width: 100%; + min-height: 100px; + margin-bottom: 10px; + } } diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.html index 1cc7d8e98b..bde9f411f9 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.html @@ -15,7 +15,11 @@ >
- + @if ( (field.name || field?.required) && !field.leftLabels) { {{field.name | translate }} ({{field.dateDisplayFormat}}) @@ -36,12 +40,13 @@ @if (dateInputControl.invalid && dateInputControl.touched) { - + error_outline @if (dateInputControl.hasError('required')) {{{ 'FORM.FIELD.REQUIRED' | translate }}} @else if (dateInputControl.hasError('matDatepickerParse')) {{{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate: { format: field.dateDisplayFormat || field.defaultDateTimeFormat } }}} @else if (dateInputControl.hasError('matDatepickerMin')) {{{ 'FORM.FIELD.VALIDATOR.NOT_LESS_THAN' | translate: { minValue: formattedMinDate } }}} @else if (dateInputControl.hasError('matDatepickerMax')) {{{ 'FORM.FIELD.VALIDATOR.NOT_GREATER_THAN' | translate: { maxValue: formattedMaxDate } }}} } +
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html index 39a356ac8d..709d994c44 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html @@ -11,7 +11,7 @@
- + @if( (field.name || field?.required) && !field.leftLabels) { {{ field.name | translate }} } @@ -32,8 +32,9 @@ @if (propertyLoadFailed && !previewState) { - error_outline{{ 'FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED' | translate }} + error_outline{{ 'FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED' | translate }} } +
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html index eedec326ff..e994d83967 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html @@ -12,7 +12,7 @@ }
- + @if ( (field.name || this.field?.required) && !field.leftLabels) { {{ field.name | translate }} } @@ -49,12 +49,13 @@ } @if ((dropdownControl.hasError('required') && !isRestApiFailed && !variableOptionsFailed) || (!previewState && !field.readOnly && (isRestApiFailed || variableOptionsFailed))) { - + error_outline @if (dropdownControl.hasError('required') && !isRestApiFailed && !variableOptionsFailed) {{{ 'FORM.FIELD.REQUIRED' | translate }}} @else if (isRestApiFailed) {{{ 'FORM.FIELD.REST_API_FAILED' | translate: { hostname: restApiHostName } }}} @else if (variableOptionsFailed) {{{ 'FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED' | translate }}} } +
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/radio-buttons/radio-buttons-cloud.widget.scss b/lib/process-services-cloud/src/lib/form/components/widgets/radio-buttons/radio-buttons-cloud.widget.scss index ae2d2c54f7..ca6a8fae94 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/radio-buttons/radio-buttons-cloud.widget.scss +++ b/lib/process-services-cloud/src/lib/form/components/widgets/radio-buttons/radio-buttons-cloud.widget.scss @@ -45,8 +45,4 @@ word-break: break-word; } } - - &-radio-group-error-message .adf-error-container { - margin-top: 5px; - } } diff --git a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss index 7cca3876c9..d7e1aa5a21 100644 --- a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss +++ b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss @@ -63,6 +63,7 @@ } .adf-error { + padding-top: 3px; animation: slide-down-fade-in 300ms cubic-bezier(0.55, 0, 0.55, 0.2); } } diff --git a/lib/process-services-cloud/src/lib/models/filter-counters-cloud.model.ts b/lib/process-services-cloud/src/lib/models/filter-counters-cloud.model.ts new file mode 100644 index 0000000000..5725360d1a --- /dev/null +++ b/lib/process-services-cloud/src/lib/models/filter-counters-cloud.model.ts @@ -0,0 +1,55 @@ +/*! + * @license + * Copyright Š 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const FilterCounterEntityType = { + TASK: 'TASK', + PROCESS_INSTANCE: 'PROCESS_INSTANCE' +} as const; + +export type FilterCounterEntityType = (typeof FilterCounterEntityType)[keyof typeof FilterCounterEntityType]; + +export interface FilterCountersQuerySort { + field: string; + direction: string; + isProcessVariable: boolean; +} + +export interface FilterCountersQuery { + requestId: string; + status?: string[]; + assignee?: string[]; + sort?: FilterCountersQuerySort; + [criteria: string]: unknown; +} + +export type FilterCountersRequest = { + [entityType in FilterCounterEntityType]?: FilterCountersQuery[]; +}; + +export interface FilterCounterCandidate { + key?: string | null; + showCounter?: boolean; +} + +export type FilterCounters = { + [entityType in FilterCounterEntityType]?: { [requestId: string]: number }; +}; + +export interface FilterCountersResult { + counters: { [filterKey: string]: number }; + batched: boolean; +} diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.scss b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.scss index 668683bfc0..a4ed4e3cdb 100644 --- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.scss +++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.scss @@ -66,6 +66,10 @@ @include mixins.adf-error-icon; } + .adf-error { + padding-top: 3px; + } + .adf-error-animate { animation: adf-people-cloud-slide-in-down 300ms cubic-bezier(0.55, 0, 0.55, 0.2); } diff --git a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.spec.ts index 8c90e5f42a..b3ca4b78f7 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.spec.ts @@ -16,15 +16,15 @@ */ import { Component, SimpleChange } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing'; import { first, of, Subject, throwError } from 'rxjs'; import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service'; import { ProcessFiltersCloudComponent } from './process-filters-cloud.component'; import { By } from '@angular/platform-browser'; -import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service'; +import { PROCESS_FILTERS_SERVICE_TOKEN, TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service'; import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service'; import { mockProcessFilters } from '../../mock/process-filters-cloud.mock'; -import { AppConfigService, AppConfigServiceMock } from '@alfresco/adf-core'; +import { AppConfigService, AppConfigServiceMock, NoopAuthModule } from '@alfresco/adf-core'; import { ProcessListCloudService } from '../../../process-list/services/process-list-cloud.service'; import { ApolloTestingModule } from 'apollo-angular/testing'; import { HarnessLoader } from '@angular/cdk/testing'; @@ -32,39 +32,39 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatIconHarness } from '@angular/material/icon/testing'; import { ActivatedRoute, provideRouter, Router } from '@angular/router'; import { RouterTestingHarness } from '@angular/router/testing'; -import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model'; +import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service'; +import { FilterCounterEntityType, FilterCountersResult } from '../../../../models/filter-counters-cloud.model'; +import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model'; @Component({ selector: 'adf-cloud-dummy', template: '' }) class DummyComponent {} const ProcessFilterCloudServiceMock = { getProcessFilters: () => of(mockProcessFilters), - getProcessNotificationSubscription: () => of([]), filterKeyToBeRefreshed$: of(mockProcessFilters[0].key) }; describe('ProcessFiltersCloudComponent', () => { let processFilterService: ProcessFilterCloudService; + let filterCountersService: FilterCountersCloudService; + let processListService: ProcessListCloudService; let component: ProcessFiltersCloudComponent; let fixture: ComponentFixture; let getProcessFiltersSpy: jasmine.Spy; - let getProcessNotificationSubscriptionSpy: jasmine.Spy; + let getFilterCountersSpy: jasmine.Spy; + let refreshFilterCountersSpy: jasmine.Spy; + let getProcessCounterSpy: jasmine.Spy; let loader: HarnessLoader; let router: Router; const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => { TestBed.configureTestingModule({ - imports: [ProcessFiltersCloudComponent, ApolloTestingModule], + imports: [NoopAuthModule, ProcessFiltersCloudComponent, ApolloTestingModule], providers: [ { provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, + { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, { provide: AppConfigService, useClass: AppConfigServiceMock }, - { - provide: ProcessListCloudService, - useValue: { - getProcessCounter: () => of(10), - getProcessListCount: () => of(10) - } - }, + ProcessListCloudService, { provide: ProcessFilterCloudService, useValue: ProcessFilterCloudServiceMock }, provideRouter([{ path: 'process-list-cloud', component: DummyComponent }]), { @@ -88,11 +88,16 @@ describe('ProcessFiltersCloudComponent', () => { component.searchApiMethod = searchApiMethod; processFilterService = TestBed.inject(ProcessFilterCloudService); + filterCountersService = TestBed.inject(FilterCountersCloudService); + processListService = TestBed.inject(ProcessListCloudService); TestBed.inject(ActivatedRoute); router = TestBed.inject(Router); await RouterTestingHarness.create(); - getProcessFiltersSpy = spyOn(processFilterService, 'getProcessFilters').and.returnValue(of(mockProcessFilters)); - getProcessNotificationSubscriptionSpy = spyOn(processFilterService, 'getProcessNotificationSubscription').and.returnValue(of([])); + getProcessFiltersSpy = spyOn(filterCountersService, 'getProcessFilters').and.returnValue(of(mockProcessFilters)); + getFilterCountersSpy = spyOn(filterCountersService, 'getFilterCounters').and.returnValue(of({ counters: {}, batched: true })); + refreshFilterCountersSpy = spyOn(filterCountersService, 'refreshFilterCounters'); + getProcessCounterSpy = spyOn(processListService, 'getProcessCounter').and.returnValue(of(10)); + spyOn(processListService, 'getProcessListCount').and.returnValue(of(10)); }; const bindAppName = async (appName = 'my-app-1') => { @@ -463,17 +468,98 @@ describe('ProcessFiltersCloudComponent', () => { expect(component.updatedFiltersSet.has(filterKeyTest)).toBeFalsy(); }); - it('should call fetchProcessFilterCounter only if filter.showCounter is true', () => { - const filterWithCounter = { ...mockProcessFilters[0], showCounter: true }; - const filterWithoutCounter = { ...mockProcessFilters[1], showCounter: false }; - const fetchSpy = spyOn(component, 'fetchProcessFilterCounter').and.returnValue(of(42)); + it('should resolve the counter only of the filters with a counter enabled', () => { + const filterWithCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[1], showCounter: true }); + const filterWithoutCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[2], showCounter: false }); + getProcessCounterSpy.calls.reset(); component.filters = [filterWithCounter, filterWithoutCounter]; component.updateFilterCounters(); - expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(fetchSpy).toHaveBeenCalledWith(filterWithCounter); - expect(fetchSpy).not.toHaveBeenCalledWith(filterWithoutCounter); + expect(getProcessCounterSpy).toHaveBeenCalledTimes(1); + expect(getProcessCounterSpy).toHaveBeenCalledWith(filterWithCounter.appName, filterWithCounter.status); + }); + + describe('Batched counters', () => { + beforeEach(() => { + getProcessFiltersSpy.and.returnValue( + of(mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true }))) + ); + }); + + it('should read the counters of the process filters of the bound app', async () => { + await bindAppName('mock-app-name'); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false); + }); + + it('should not ask for the batched count endpoint by default', async () => { + await bindAppName('mock-app-name'); + + expect(component.useBatchedCounters).toBeFalse(); + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false); + }); + + it('should ask for the batched count endpoint when the input is set', async () => { + fixture.componentRef.setInput('useBatchedCounters', true); + + await bindAppName('mock-app-name'); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, true); + }); + + it('should hold the counters resolved by the batched count request', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + + await bindAppName('mock-app-name'); + + expect(component.counters['FakeRunningProcesses']).toBe(9); + }); + + it('should emit the filters whose counter changed', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); + + await bindAppName('mock-app-name'); + + expect(updatedFilterSpy).toHaveBeenCalledWith('FakeRunningProcesses'); + }); + + it('should resolve the counters one filter at a time when the batched endpoint is not available', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false })); + + await bindAppName('mock-app-name'); + + expect(getProcessCounterSpy).toHaveBeenCalledTimes(3); + expect(component.counters['FakeRunningProcesses']).toBe(10); + }); + + it('should resolve the counters of the filters the batch left out on their own', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + + await bindAppName('mock-app-name'); + + expect(component.counters['FakeRunningProcesses']).toBe(9); + expect(getProcessCounterSpy.calls.allArgs().map(([, status]) => status)).toEqual([null, 'COMPLETED']); + }); + + it('should keep the counters of the other filters when one counter cannot be resolved', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + getProcessCounterSpy.and.throwError('the query of the filter cannot be built'); + + await bindAppName('mock-app-name'); + + expect(component.counters['FakeRunningProcesses']).toBe(9); + expect(component.counters['completed-processes']).toBe(0); + }); + + it('should refresh the counters of every filter when a filter is clicked', async () => { + await bindAppName('mock-app-name'); + + component.onFilterClick(mockProcessFilters[1]); + + expect(refreshFilterCountersSpy).toHaveBeenCalledWith('mock-app-name'); + }); }); describe('Notifications config', () => { @@ -507,39 +593,436 @@ describe('ProcessFiltersCloudComponent', () => { expect(component.notificationDebounceTime).toBe(5000); }); - it('should debounce notification subscription using the configured debounce time', fakeAsync(() => { - const notifications$ = new Subject(); - getProcessNotificationSubscriptionSpy.and.returnValue(notifications$.asObservable()); + it('should keep the counters in sync with the counters stream', fakeAsync(() => { + const counters$ = new Subject(); + getFilterCountersSpy.and.returnValue(counters$.asObservable()); component.appName = 'mock-app-name'; fixture.detectChanges(); + component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true })); - const updateFilterCountersSpy = spyOn(component, 'updateFilterCounters'); - - notifications$.next([]); - tick(1000); - expect(updateFilterCountersSpy).not.toHaveBeenCalled(); - - tick(2000); - expect(updateFilterCountersSpy).toHaveBeenCalledTimes(1); + counters$.next({ counters: { FakeRunningProcesses: 7 }, batched: true }); + expect(component.counters['FakeRunningProcesses']).toBe(7); flush(); })); }); describe('Highlight Selected Filter', () => { - it('should make subscription', async () => { + const allProcessesFilterKey = mockProcessFilters[0].key; + const allProcessesFilterId = mockProcessFilters[0].id; + + it('should apply active CSS class on filter click', async () => { component.enableNotifications = true; await bindAppName('mock-app-name'); - expect(getProcessNotificationSubscriptionSpy).toHaveBeenCalled(); + + let link = fixture.debugElement.query(By.css(`[data-automation-id="${allProcessesFilterKey}_filter"]`)).nativeElement; + expect(link.getAttribute('href')).toBe(`/process-list-cloud?filterId=${allProcessesFilterId}`); + + link.click(); + fixture.detectChanges(); + await fixture.whenStable(); + expect(router.url).toBe(`/process-list-cloud?filterId=${allProcessesFilterId}`); + + link = fixture.debugElement.query(By.css(`[data-automation-id="${allProcessesFilterKey}_filter"]`)).nativeElement; + expect(link.classList).toContain('adf-active'); }); - it('should not make subscription when notifications are disabled', async () => { - const appConfigService = TestBed.inject(AppConfigService); - spyOn(appConfigService, 'get').and.callFake((key: string, defaultValue: any) => (key === 'notifications' ? false : defaultValue)); + it('should add aria-current attribute with value "page" to the active filter', async () => { + component.enableNotifications = true; await bindAppName('mock-app-name'); - expect(getProcessNotificationSubscriptionSpy).not.toHaveBeenCalled(); + const link = fixture.debugElement.query(By.css(`[data-automation-id="${allProcessesFilterKey}_filter"]`)).nativeElement; + expect(link.getAttribute('aria-current')).toBe('page'); + }); + + it('should not have aria-current attribute when filter is not active', async () => { + component.enableNotifications = true; + await bindAppName('mock-app-name'); + + const link = fixture.debugElement.query(By.css(`[data-automation-id="${mockProcessFilters[1].key}_filter"]`)).nativeElement; + expect(link.getAttribute('aria-current')).toBeNull(); + }); + }); + }); + + describe('searchApiMethod set to POST', () => { + beforeEach(async () => { + await configureTestingModule('POST'); + }); + + it('should attach specific icon for each filter if hasIcon is true', async () => { + await bindAppName(); + + component.showIcons = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.filters.length).toBe(3); + const filterIcons = await loader.getAllHarnesses(MatIconHarness.with({ selector: '[data-automation-id="adf-filter-icon"]' })); + expect(filterIcons.length).toBe(3); + expect(await filterIcons[0].getName()).toContain('adjust'); + expect(await filterIcons[1].getName()).toContain('inbox'); + expect(await filterIcons[2].getName()).toContain('done'); + }); + + it('should not attach icons for each filter if hasIcon is false', async () => { + component.showIcons = false; + await bindAppName(); + + const filterIcons = await loader.getAllHarnesses(MatIconHarness.with({ selector: '[data-automation-id="adf-filter-icon"]' })); + expect(filterIcons.length).toBe(0); + }); + + it('should display the filters', async () => { + await bindAppName(); + + component.showIcons = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + const filters = fixture.debugElement.queryAll(By.css('.adf-process-filters__entry')); + expect(component.filters.length).toBe(3); + expect(filters.length).toBe(3); + expect(filters[0].nativeElement.innerText).toContain('FakeAllProcesses'); + expect(filters[1].nativeElement.innerText).toContain('FakeRunningProcesses'); + expect(filters[2].nativeElement.innerText).toContain('FakeCompletedProcesses'); + expect(Object.keys(component.counters).length).toBe(3); + }); + + it('should emit success with the filters when filters are loaded', async () => { + const successSpy = spyOn(component.success, 'emit'); + await bindAppName(); + + expect(successSpy).toHaveBeenCalledWith(mockProcessFilters); + expect(component.filters).toBeDefined(); + expect(component.filters[0].name).toEqual('FakeAllProcesses'); + expect(component.filters[1].name).toEqual('FakeRunningProcesses'); + expect(component.filters[2].name).toEqual('FakeCompletedProcesses'); + expect(Object.keys(component.counters).length).toBe(3); + }); + + it('should not select any filter as default', async () => { + await bindAppName(); + + expect(component.currentFilter).toBeUndefined(); + }); + + it('should filterClicked emit when a filter is clicked from the UI', async () => { + const filterClickedSpy = spyOn(component.filterClicked, 'emit'); + await bindAppName(); + + const filterButton = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${mockProcessFilters[0].key}_filter"]`); + filterButton.click(); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.currentFilter).toEqual(mockProcessFilters[0]); + expect(filterClickedSpy).toHaveBeenCalledWith(mockProcessFilters[0]); + }); + }); + + describe('API agnostic', () => { + beforeEach(async () => { + await configureTestingModule('GET'); + }); + + it('should emit an error with a bad response', async () => { + getProcessFiltersSpy.and.returnValue(throwError('wrong request')); + let lastValue: any; + component.error.subscribe((err) => (lastValue = err)); + + await bindAppName(); + + expect(lastValue).toBeDefined(); + }); + + it('should not select any process filter if filter input does not exist', async () => { + const change = new SimpleChange(null, { name: 'nonexistentFilter' }, true); + fixture.detectChanges(); + await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toBeUndefined(); + }); + + it('should select the filter based on the input by name param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { name: 'FakeRunningProcesses' }, true); + + await bindAppName(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(mockProcessFilters[1]); + expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[1]); + }); + + it('should select the filter based on the input by key param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { key: 'completed-processes' }, true); + + await bindAppName(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(mockProcessFilters[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]); + }); + + it('should select the filter based on the input by index param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { index: 2 }, true); + + await bindAppName(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(mockProcessFilters[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]); + }); + + it('should select the filter based on the input by id param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { id: '12' }, true); + + await bindAppName(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(mockProcessFilters[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]); + }); + + it('should reset the filter when the param is undefined', () => { + const change = new SimpleChange(mockProcessFilters[0], undefined, false); + component.currentFilter = mockProcessFilters[0]; + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(undefined); + }); + + it('should not emit a filter clicked event when a filter is selected through the filterParam input (filterClicked emits only through a UI click action)', async () => { + const filterClickedSpy = spyOn(component.filterClicked, 'emit'); + const change = new SimpleChange(null, { id: '10' }, true); + + await bindAppName(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toBe(mockProcessFilters[0]); + expect(filterClickedSpy).not.toHaveBeenCalled(); + }); + + it('should reload filters by appName on binding changes', () => { + spyOn(component, 'getFilters').and.stub(); + const appName = 'my-app-1'; + + const change = new SimpleChange(null, appName, true); + component.ngOnChanges({ appName: change }); + + expect(component.getFilters).toHaveBeenCalledWith(appName); + }); + + it('should not reload filters by appName null on binding changes', () => { + spyOn(component, 'getFilters').and.stub(); + const appName = null; + + const change = new SimpleChange(undefined, appName, true); + component.ngOnChanges({ appName: change }); + + expect(component.getFilters).not.toHaveBeenCalledWith(appName); + }); + + it('should reload filters by app name on binding changes', () => { + spyOn(component, 'getFilters').and.stub(); + const appName = 'fake-app-name'; + + const change = new SimpleChange(null, appName, true); + component.ngOnChanges({ appName: change }); + + expect(component.getFilters).toHaveBeenCalledWith(appName); + }); + + it('should return the current filter after one is selected', () => { + const filter = mockProcessFilters[1]; + component.filters = mockProcessFilters; + + expect(component.currentFilter).toBeUndefined(); + component.selectFilter({ id: filter.id }); + expect(component.getCurrentFilter()).toBe(filter); + }); + + it('should remove key from set of updated filters when received refreshed filter key', async () => { + const filterKeyTest = 'filter-key-test'; + component.updatedFiltersSet.add(filterKeyTest); + + expect(component.updatedFiltersSet.size).toBe(1); + processFilterService.filterKeyToBeRefreshed$ = of(filterKeyTest); + fixture.detectChanges(); + + expect(component.updatedFiltersSet.has(filterKeyTest)).toBeFalsy(); + }); + + it('should resolve the counter only of the filters with a counter enabled', () => { + const filterWithCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[1], showCounter: true }); + const filterWithoutCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[2], showCounter: false }); + getProcessCounterSpy.calls.reset(); + + component.filters = [filterWithCounter, filterWithoutCounter]; + component.updateFilterCounters(); + + expect(getProcessCounterSpy).toHaveBeenCalledTimes(1); + expect(getProcessCounterSpy).toHaveBeenCalledWith(filterWithCounter.appName, filterWithCounter.status); + }); + + describe('Batched counters', () => { + beforeEach(() => { + getProcessFiltersSpy.and.returnValue( + of(mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true }))) + ); + }); + + it('should read the counters of the process filters of the bound app', async () => { + await bindAppName('mock-app-name'); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false); + }); + + it('should not ask for the batched count endpoint by default', async () => { + await bindAppName('mock-app-name'); + + expect(component.useBatchedCounters).toBeFalse(); + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false); + }); + + it('should ask for the batched count endpoint when the input is set', async () => { + fixture.componentRef.setInput('useBatchedCounters', true); + + await bindAppName('mock-app-name'); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, true); + }); + + it('should hold the counters resolved by the batched count request', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + + await bindAppName('mock-app-name'); + + expect(component.counters['FakeRunningProcesses']).toBe(9); + }); + + it('should emit the filters whose counter changed', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); + + await bindAppName('mock-app-name'); + + expect(updatedFilterSpy).toHaveBeenCalledWith('FakeRunningProcesses'); + }); + + it('should resolve the counters one filter at a time when the batched endpoint is not available', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false })); + + await bindAppName('mock-app-name'); + + expect(getProcessCounterSpy).toHaveBeenCalledTimes(3); + expect(component.counters['FakeRunningProcesses']).toBe(10); + }); + + it('should resolve the counters of the filters the batch left out on their own', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + + await bindAppName('mock-app-name'); + + expect(component.counters['FakeRunningProcesses']).toBe(9); + expect(getProcessCounterSpy.calls.allArgs().map(([, status]) => status)).toEqual([null, 'COMPLETED']); + }); + + it('should keep the counters of the other filters when one counter cannot be resolved', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + getProcessCounterSpy.and.throwError('the query of the filter cannot be built'); + + await bindAppName('mock-app-name'); + + expect(component.counters['FakeRunningProcesses']).toBe(9); + expect(component.counters['completed-processes']).toBe(0); + }); + + it('should refresh the counters of every filter when a filter is clicked', async () => { + await bindAppName('mock-app-name'); + + component.onFilterClick(mockProcessFilters[1]); + + expect(refreshFilterCountersSpy).toHaveBeenCalledWith('mock-app-name'); + }); + }); + + describe('Notifications config', () => { + it('should read enableNotifications and notificationDebounceTime from app config on init', () => { + const appConfigService = TestBed.inject(AppConfigService); + const getSpy = spyOn(appConfigService, 'get').and.callThrough(); + + fixture.detectChanges(); + + expect(getSpy).toHaveBeenCalledWith('notifications', true); + expect(getSpy).toHaveBeenCalledWith('notificationDebounceTime', 3000); + }); + + it('should default notificationDebounceTime to 3000 when not set in app config', () => { + fixture.detectChanges(); + + expect(component.notificationDebounceTime).toBe(3000); + }); + + it('should use notificationDebounceTime from app config', () => { + const appConfigService: AppConfigService = TestBed.inject(AppConfigService); + spyOn(appConfigService, 'get').and.callFake((key: string, defaultValue: any) => { + if (key === 'notificationDebounceTime') { + return 5000; + } + return defaultValue; + }); + + fixture.detectChanges(); + + expect(component.notificationDebounceTime).toBe(5000); + }); + + it('should keep the counters in sync with the counters stream', fakeAsync(() => { + const counters$ = new Subject(); + getFilterCountersSpy.and.returnValue(counters$.asObservable()); + component.appName = 'mock-app-name'; + + fixture.detectChanges(); + component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true })); + + counters$.next({ counters: { FakeRunningProcesses: 7 }, batched: true }); + + expect(component.counters['FakeRunningProcesses']).toBe(7); + flush(); + })); + + it('should resolve the counters one filter at a time when the batched endpoint is not available', fakeAsync(() => { + const counters$ = new Subject(); + getFilterCountersSpy.and.returnValue(counters$.asObservable()); + component.appName = 'mock-app-name'; + + fixture.detectChanges(); + component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true })); + getProcessCounterSpy.calls.reset(); + + counters$.next({ counters: {}, batched: false }); + + expect(getProcessCounterSpy).toHaveBeenCalledTimes(3); + flush(); + })); + }); + + describe('Highlight Selected Filter', () => { + it('should read the counters of the bound app', async () => { + component.enableNotifications = true; + await bindAppName('mock-app-name'); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false); }); it('should emit filter key when filter counter is set for first time', () => { diff --git a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.ts b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.ts index 9e7fc7ba5f..d7001220d4 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.ts @@ -16,14 +16,16 @@ */ import { Component, DestroyRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; -import { EMPTY, Observable } from 'rxjs'; +import { combineLatest, defer, EMPTY, Observable, of, Subscription } from 'rxjs'; import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service'; import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model'; import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core'; import { FilterParamsModel } from '../../../../task/task-filters/models/filter-cloud.model'; -import { catchError, debounceTime, map, shareReplay, tap } from 'rxjs/operators'; +import { catchError, map } from 'rxjs/operators'; import { ProcessListCloudService } from '../../../process-list/services/process-list-cloud.service'; import { ProcessFilterCloudAdapter } from '../../../process-list/models/process-cloud-query-request.model'; +import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service'; +import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { TranslatePipe } from '@ngx-translate/core'; import { AsyncPipe } from '@angular/common'; @@ -43,10 +45,21 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { @Input() appName: string = ''; - /** (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the process count */ + /** + * (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the process count. + * + */ @Input() searchApiMethod: 'GET' | 'POST' = 'GET'; + /** + * (optional) Resolves the counters of the task and the process filters with a single call to + * `POST /query/v1/count`. Both filter components have to + * ask for it, otherwise the counters are resolved one filter at a time. + */ + @Input() + useBatchedCounters = false; + /** (optional) The filter to be selected by default */ @Input() filterParam: FilterParamsModel; @@ -79,27 +92,31 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { currentFilter?: ProcessFilterCloudModel; filters: ProcessFilterCloudModel[] = []; counters: { [key: string]: number } = {}; - enableNotifications = true; - notificationDebounceTime = 3000; currentFiltersValues: { [key: string]: number } = {}; updatedFiltersSet = new Set(); + enableNotifications = true; + notificationDebounceTime = 3000; private filtersLoadedFor?: string; + private countersSubscription?: Subscription; + private countersFilters$?: Observable; + private batchedCounters = true; private readonly destroyRef = inject(DestroyRef); private readonly processFilterCloudService = inject(ProcessFilterCloudService); private readonly translationService = inject(TranslationService); private readonly appConfigService = inject(AppConfigService); private readonly processListCloudService = inject(ProcessListCloudService); + private readonly filterCountersCloudService = inject(FilterCountersCloudService); private readonly activatedRoute = inject(ActivatedRoute); protected readonly currentRouteFilterId = toSignal(this.activatedRoute.queryParamMap.pipe(map((params) => params.get('filterId')))); ngOnInit() { this.enableNotifications = this.appConfigService.get('notifications', true); this.notificationDebounceTime = this.appConfigService.get('notificationDebounceTime', 3000); + if (!this.filtersLoadedFor) { this.getFilters(this.appName); } - this.initProcessNotification(); this.getFilterKeysAfterExternalRefreshing(); } @@ -110,6 +127,8 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { this.getFilters(appName.currentValue); } else if (filter && filter.currentValue !== filter.previousValue) { this.selectFilterAndEmit(filter.currentValue); + } else if (changes['useBatchedCounters'] && !changes['useBatchedCounters'].firstChange && this.filtersLoadedFor) { + this.loadFilterCounters(this.filtersLoadedFor); } } @@ -120,8 +139,8 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { */ getFilters(appName: string): void { this.filtersLoadedFor = appName; - const filters$ = this.processFilterCloudService.getProcessFilters(appName).pipe(shareReplay({ bufferSize: 1, refCount: true })); - this.filters$ = filters$.pipe(catchError(() => EMPTY)); + const filters$ = this.filterCountersCloudService.getProcessFilters(appName); + this.filters$ = filters$.pipe(catchError(() => of([]))); filters$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (res) => { @@ -130,19 +149,25 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { this.initFilterCounters(); this.selectFilterAndEmit(this.filterParam); this.success.emit(res); - this.updateFilterCounters(); }, - error: (err: any) => { + error: (err: unknown) => { this.error.emit(err); } }); + + this.countersFilters$ = filters$; + this.loadFilterCounters(appName); } /** * Initialize counter collection for filters */ - initFilterCounters() { - this.filters.forEach((filter) => (this.counters[filter.key] = 0)); + initFilterCounters(): void { + this.filters.forEach((filter) => { + if (filter.key) { + this.counters[filter.key] = 0; + } + }); } /** @@ -167,20 +192,6 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { ); // fallback to preserve the previous behavior } - /** - * Check equality of the filter names by translating the given name strings - * - * @param name1 source name - * @param name2 target name - * @returns `true` if filter names are equal, otherwise `false` - */ - private checkFilterNamesEquality(name1: string, name2: string): boolean { - const translatedName1 = this.translationService.instant(name1); - const translatedName2 = this.translationService.instant(name2); - - return translatedName1.toLocaleLowerCase() === translatedName2.toLocaleLowerCase(); - } - /** * Selects and emits the given filter * @@ -213,7 +224,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { if (filter) { this.selectFilter(filter); this.filterClicked.emit(this.currentFilter); - this.updateFilterCounter(this.currentFilter); + this.refreshFilterCounter(this.currentFilter); this.updatedFiltersSet.delete(filter.key); } else { this.currentFilter = undefined; @@ -247,6 +258,83 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { return this.filters === undefined || (this.filters && this.filters.length === 0); } + isActiveFilter(filter: ProcessFilterCloudModel): boolean { + return this.currentFilter.name === filter.name; + } + + /** + * @deprecated does nothing: the counters keep themselves in sync. Removed in ADF 10.0.0. + */ + initProcessNotification(): void {} + + /** + * Iterate over filters and update counters + * + * @deprecated counts one filter at a time. Removed in ADF 10.0.0. + */ + updateFilterCounters(): void { + this.filters.forEach((filter) => this.updateFilterCounter(filter)); + } + + /** + * Get current value for filter and check if value has changed + * + * @param filter filter + * @deprecated counts one filter at a time. Removed in ADF 10.0.0. + */ + updateFilterCounter(filter: ProcessFilterCloudModel): void { + const filterKey = filter?.showCounter ? filter.key : undefined; + if (!filterKey) { + return; + } + + defer(() => this.fetchProcessFilterCounter(filter)) + .pipe( + catchError(() => EMPTY), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe((counter) => { + this.checkIfFilterValuesHasBeenUpdated(filterKey, counter); + this.counters = { ...this.counters, [filterKey]: counter }; + }); + } + + checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number): void { + if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) { + this.currentFiltersValues = { ...this.currentFiltersValues, [filterKey]: filterValue }; + this.updatedFilter.emit(filterKey); + this.updatedFiltersSet.add(filterKey); + } + } + + /** + * Get filer key when filter was refreshed by external action + * + */ + getFilterKeysAfterExternalRefreshing(): void { + this.processFilterCloudService.filterKeyToBeRefreshed$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((filterKey: string) => this.updatedFiltersSet.delete(filterKey)); + } + + isFilterUpdated(filterName: string): boolean { + return this.updatedFiltersSet.has(filterName); + } + + /** + * Check equality of the filter names by translating the given name strings + * + * @param name1 source name + * @param name2 target name + * @returns `true` if filter names are equal, otherwise `false` + */ + private checkFilterNamesEquality(name1: string, name2: string): boolean { + const translatedName1 = this.translationService.instant(name1); + const translatedName2 = this.translationService.instant(name2); + + return translatedName1.toLocaleLowerCase() === translatedName2.toLocaleLowerCase(); + } + /** * Reset the filters */ @@ -255,76 +343,53 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { this.currentFilter = undefined; } - isActiveFilter(filter: ProcessFilterCloudModel): boolean { - return this.currentFilter.name === filter.name; - } - - initProcessNotification(): void { - if (this.appName && this.enableNotifications) { - this.processFilterCloudService - .getProcessNotificationSubscription(this.appName) - .pipe(debounceTime(this.notificationDebounceTime), takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.updateFilterCounters(); - }); - } - } - - /** - * Iterate over filters and update counters - */ - updateFilterCounters(): void { - this.filters.forEach((filter: ProcessFilterCloudModel) => { - this.updateFilterCounter(filter); - }); - } - - /** - * Get current value for filter and check if value has changed - * - * @param filter filter - */ - updateFilterCounter(filter: ProcessFilterCloudModel): void { - if (!filter?.showCounter) { + private loadFilterCounters(appName: string): void { + if (!this.countersFilters$) { return; } - this.fetchProcessFilterCounter(filter) - .pipe( - tap((filterCounter) => { - this.checkIfFilterValuesHasBeenUpdated(filter.key, filterCounter); - }) - ) - .subscribe((data) => { - this.counters = { - ...this.counters, - [filter.key]: data - }; + this.countersSubscription?.unsubscribe(); + this.countersSubscription = combineLatest([ + this.countersFilters$.pipe(catchError(() => of([]))), + this.filterCountersCloudService.getFilterCounters(appName, FilterCounterEntityType.PROCESS_INSTANCE, this.useBatchedCounters) + ]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(([, { counters, batched }]) => { + this.batchedCounters = batched; + if (batched) { + this.applyFilterCounters(counters); + } else { + this.updateFilterCounters(); + } }); } - checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number): void { - if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) { - this.currentFiltersValues[filterKey] = filterValue; - this.updatedFilter.emit(filterKey); - this.updatedFiltersSet.add(filterKey); - } - } + private applyFilterCounters(counters: { [filterKey: string]: number }): void { + this.filters.forEach((filter) => { + const filterKey = filter?.showCounter ? filter.key : undefined; + if (!filterKey) { + return; + } - isFilterUpdated(filterName: string): boolean { - return this.updatedFiltersSet.has(filterName); - } + const counter = counters[filterKey]; + if (counter === undefined) { + this.updateFilterCounter(filter); + return; + } - /** - * Get filer key when filter was refreshed by external action - * - */ - getFilterKeysAfterExternalRefreshing(): void { - this.processFilterCloudService.filterKeyToBeRefreshed$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((filterKey: string) => { - this.updatedFiltersSet.delete(filterKey); + this.checkIfFilterValuesHasBeenUpdated(filterKey, counter); + this.counters = { ...this.counters, [filterKey]: counter }; }); } + private refreshFilterCounter(filter?: ProcessFilterCloudModel): void { + if (this.batchedCounters) { + this.filterCountersCloudService.refreshFilterCounters(this.appName); + } else if (filter) { + this.updateFilterCounter(filter); + } + } + private fetchProcessFilterCounter(filter: ProcessFilterCloudModel): Observable { return this.searchApiMethod === 'POST' ? this.processListCloudService.getProcessListCount(new ProcessFilterCloudAdapter(filter)) diff --git a/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.ts b/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.ts index 316933cba6..9839470ea2 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.ts @@ -404,6 +404,12 @@ export class ProcessFilterCloudService { ]; } + /** + * @deprecated use FilterCountersCloudService.getEngineEvents instead. + * + * @param appName Name of the target app + * @returns Process engine events + */ getProcessNotificationSubscription(appName: string): Observable { return this.notificationCloudService .makeGQLQuery(appName, PROCESS_EVENT_SUBSCRIPTION_QUERY) diff --git a/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.ts b/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.ts index 84c5a97cfb..8316bf1f09 100644 --- a/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.ts @@ -100,7 +100,7 @@ export class ProcessListCloudService extends BaseCloudService { ); } - protected buildQueryData(requestNode: ProcessListRequestModel): { [key: string]: any } { + buildQueryData(requestNode: ProcessListRequestModel): { [key: string]: any } { const queryData: { [key: string]: any } = { name: requestNode.name, id: requestNode.id, diff --git a/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.spec.ts new file mode 100644 index 0000000000..c0846418f6 --- /dev/null +++ b/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.spec.ts @@ -0,0 +1,651 @@ +/*! + * @license + * Copyright Š 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core'; +import { BehaviorSubject, combineLatest, firstValueFrom, Observable, of, Subject, throwError } from 'rxjs'; +import { ApolloTestingModule } from 'apollo-angular/testing'; +import { FilterCountersCloudService } from './filter-counters-cloud.service'; +import { NotificationCloudService } from './notification-cloud.service'; +import { LocalPreferenceCloudService } from './local-preference-cloud.service'; +import { PROCESS_FILTERS_SERVICE_TOKEN, TASK_FILTERS_SERVICE_TOKEN } from './cloud-token.service'; +import { TaskFilterCloudService } from '../task/task-filters/services/task-filter-cloud.service'; +import { ProcessFilterCloudService } from '../process/process-filters/services/process-filter-cloud.service'; +import { TaskFilterCloudModel } from '../task/task-filters/models/filter-cloud.model'; +import { ProcessFilterCloudModel } from '../process/process-filters/models/process-filter-cloud.model'; +import { + FilterCounterEntityType, + FilterCounters, + FilterCountersQuery, + FilterCountersRequest, + FilterCountersResult +} from '../models/filter-counters-cloud.model'; +import { TaskCloudEngineEvent } from '../models/engine-event-cloud.model'; +import { FetchResult } from '@apollo/client/core'; + +type EngineEventsResult = FetchResult<{ engineEvents?: TaskCloudEngineEvent[] }>; + +interface CountEndpoint { + post: (url: string, request: FilterCountersRequest) => Observable; +} + +describe('FilterCountersCloudService', () => { + let service: FilterCountersCloudService; + let notificationCloudService: NotificationCloudService; + let appConfigService: AppConfigService; + let taskEvents$: Subject; + let processEvents$: Subject; + let makeGQLQuerySpy: jasmine.Spy; + let postSpy: jasmine.Spy; + let getTaskListFiltersSpy: jasmine.Spy; + let getProcessFiltersSpy: jasmine.Spy; + + const countRequest = (): FilterCountersRequest => postSpy.calls.mostRecent().args[1]; + const countUrl = (): string => postSpy.calls.mostRecent().args[0]; + const countQueries = (entityType: FilterCounterEntityType): FilterCountersQuery[] => countRequest()[entityType] ?? []; + const countRequestIds = (entityType: FilterCounterEntityType): string[] => countQueries(entityType).map((query) => query.requestId); + + const countersMock: FilterCounters = { + TASK: { 'my-tasks': 5, 'queued-tasks': 0 }, + PROCESS_INSTANCE: { 'running-processes': 5 } + }; + + const taskFilter = (filter: Partial) => + new TaskFilterCloudModel({ appName: 'mock-app', sort: 'createdDate', order: 'DESC', ...filter }); + const processFilter = (filter: Partial) => + new ProcessFilterCloudModel({ appName: 'mock-app', sort: 'startDate', order: 'DESC', ...filter }); + + const taskFiltersMock = [ + taskFilter({ key: 'my-tasks', status: 'ASSIGNED', assignee: 'mock-user', showCounter: true }), + taskFilter({ key: 'queued-tasks', status: 'CREATED', showCounter: true }), + taskFilter({ key: 'completed-tasks', status: 'COMPLETED', showCounter: false }) + ]; + const processFiltersMock = [ + processFilter({ key: 'running-processes', status: 'RUNNING', showCounter: true }), + processFilter({ key: 'all-processes', status: '', showCounter: false }) + ]; + + const engineEvents = (eventType: string): EngineEventsResult => ({ + data: { engineEvents: [{ eventType, entity: {} } as TaskCloudEngineEvent] } + }); + const emitTaskEvent = (eventType = 'TASK_CREATED') => taskEvents$.next(engineEvents(eventType)); + const emitProcessEvent = (eventType = 'PROCESS_STARTED') => processEvents$.next(engineEvents(eventType)); + + const counters = (entityType: FilterCounterEntityType, appName = 'mock-app') => + firstValueFrom(service.getFilterCounters(appName, entityType, true)); + const taskCounters = (appName = 'mock-app') => counters(FilterCounterEntityType.TASK, appName); + const processCounters = (appName = 'mock-app') => counters(FilterCounterEntityType.PROCESS_INSTANCE, appName); + const bothCounters = (appName = 'mock-app') => + firstValueFrom( + combineLatest([ + service.getFilterCounters(appName, FilterCounterEntityType.TASK, true), + service.getFilterCounters(appName, FilterCounterEntityType.PROCESS_INSTANCE, true) + ]) + ); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NoopAuthModule, ApolloTestingModule], + providers: [ + { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, + { provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService } + ] + }); + + service = TestBed.inject(FilterCountersCloudService); + notificationCloudService = TestBed.inject(NotificationCloudService); + appConfigService = TestBed.inject(AppConfigService); + appConfigService.config.bpmHost = 'https://fake-bpm-host.com'; + + taskEvents$ = new Subject(); + processEvents$ = new Subject(); + makeGQLQuerySpy = spyOn(notificationCloudService, 'makeGQLQuery'); + makeGQLQuerySpy.and.callFake((_appName: string, query: string) => + (query.includes('TASK_CREATED') ? taskEvents$ : processEvents$).asObservable() + ); + postSpy = spyOn(service as unknown as CountEndpoint, 'post').and.returnValue(of(countersMock)); + getTaskListFiltersSpy = spyOn(TestBed.inject(TaskFilterCloudService), 'getTaskListFilters').and.returnValue(of(taskFiltersMock)); + getProcessFiltersSpy = spyOn(TestBed.inject(ProcessFilterCloudService), 'getProcessFilters').and.returnValue(of(processFiltersMock)); + }); + + describe('getTaskFilters / getProcessFilters', () => { + it('should load the filters of every entity type', async () => { + expect(await firstValueFrom(service.getTaskFilters('mock-app'))).toEqual(taskFiltersMock); + expect(await firstValueFrom(service.getProcessFilters('mock-app'))).toEqual(processFiltersMock); + }); + + it('should load the filters of an app once for concurrent subscribers', async () => { + await firstValueFrom(combineLatest([service.getTaskFilters('mock-app'), service.getTaskFilters('mock-app')])); + await firstValueFrom(combineLatest([service.getProcessFilters('mock-app'), service.getProcessFilters('mock-app')])); + + expect(getTaskListFiltersSpy).toHaveBeenCalledTimes(1); + expect(getProcessFiltersSpy).toHaveBeenCalledTimes(1); + }); + + it('should load the filters of every app', async () => { + await firstValueFrom(service.getTaskFilters('mock-app')); + await firstValueFrom(service.getTaskFilters('other-app')); + + expect(getTaskListFiltersSpy.calls.allArgs()).toEqual([['mock-app'], ['other-app']]); + }); + + it('should share the filters with the batched count request', async () => { + const subscription = service.getTaskFilters('mock-app').subscribe(); + await taskCounters(); + subscription.unsubscribe(); + + expect(getTaskListFiltersSpy).toHaveBeenCalledTimes(1); + }); + + it('should propagate the error of the filters that fail to load', async () => { + getTaskListFiltersSpy.and.returnValue(throwError(() => new Error('filters failed'))); + + await expectAsync(firstValueFrom(service.getTaskFilters('mock-app'))).toBeRejectedWithError('filters failed'); + }); + }); + + describe('getFilterCounters', () => { + it('should return EMPTY when appName is not set', () => { + let completed = false; + service.getFilterCounters('', FilterCounterEntityType.TASK).subscribe({ complete: () => (completed = true) }); + + expect(completed).toBeTrue(); + expect(postSpy).not.toHaveBeenCalled(); + }); + + it('should resolve the counters of both entity types with a single request', async () => { + expect(await bothCounters()).toEqual([ + { counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true }, + { counters: { 'running-processes': 5 }, batched: true } + ]); + + expect(postSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the batched count endpoint of the app', async () => { + await taskCounters(); + + expect(countUrl()).toBe('https://fake-bpm-host.com/mock-app/query/v1/count'); + }); + + it('should identify the query of every filter by the key of the filter', async () => { + await bothCounters(); + + expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']); + expect(countRequestIds(FilterCounterEntityType.PROCESS_INSTANCE)).toEqual(['running-processes']); + }); + + it('should send the criteria of every filter along with its request id', async () => { + await taskCounters(); + + expect(countQueries(FilterCounterEntityType.TASK)[0]).toEqual({ + requestId: 'my-tasks', + status: ['ASSIGNED'], + assignee: ['mock-user'], + sort: { field: 'createdDate', direction: 'desc', isProcessVariable: false } + }); + }); + + it('should not send the filters without a counter enabled', async () => { + await taskCounters(); + + expect(countRequestIds(FilterCounterEntityType.TASK)).not.toContain('completed-tasks'); + }); + + it('should send the query of a filter targeting every status', async () => { + getProcessFiltersSpy.and.returnValue(of([processFilter({ key: 'all-processes', status: '', showCounter: true })])); + + await processCounters(); + + expect(countRequestIds(FilterCounterEntityType.PROCESS_INSTANCE)).toEqual(['all-processes']); + }); + + it('should omit an entity type without filters with a counter enabled', async () => { + getProcessFiltersSpy.and.returnValue(of([])); + + await bothCounters(); + + expect(countRequest().PROCESS_INSTANCE).toBeUndefined(); + }); + + it('should leave out a filter the query cannot be built for', async () => { + getTaskListFiltersSpy.and.returnValue( + of([taskFilter({ key: 'broken', status: 'ASSIGNED', showCounter: true, sort: undefined, order: undefined }), taskFiltersMock[1]]) + ); + + await taskCounters(); + + expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['queued-tasks']); + }); + + it('should leave out a filter without a key, since it holds no request id', async () => { + getProcessFiltersSpy.and.returnValue(of([processFilter({ key: null, status: 'RUNNING', showCounter: true })])); + + expect(await processCounters()).toEqual({ counters: {}, batched: true }); + expect(postSpy).not.toHaveBeenCalled(); + }); + + it('should resolve the counters of an entity type when the filters of the other one fail to load', async () => { + getProcessFiltersSpy.and.returnValue(throwError(() => new Error('filters failed'))); + + await bothCounters(); + + expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']); + expect(countRequest().PROCESS_INSTANCE).toBeUndefined(); + }); + + it('should resolve no counter when no filter has a counter enabled', async () => { + getTaskListFiltersSpy.and.returnValue(of([])); + getProcessFiltersSpy.and.returnValue(of([])); + + expect(await taskCounters()).toEqual({ counters: {}, batched: true }); + expect(postSpy).not.toHaveBeenCalled(); + }); + + describe('when the batched count endpoint is not available', () => { + it('should report the counters as not batched', async () => { + postSpy.and.returnValue(throwError(() => ({ status: 404 }))); + + expect(await taskCounters()).toEqual({ counters: {}, batched: false }); + }); + + it('should not call the endpoint again for the same app', async () => { + postSpy.and.returnValue(throwError(() => ({ status: 404 }))); + + await taskCounters(); + expect(await processCounters()).toEqual({ counters: {}, batched: false }); + + expect(postSpy).toHaveBeenCalledTimes(1); + }); + + it('should keep calling the endpoint of the apps that do hold it', async () => { + postSpy.and.returnValue(throwError(() => ({ status: 404 }))); + await taskCounters(); + + postSpy.and.returnValue(of(countersMock)); + expect(await taskCounters('other-app')).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true }); + }); + + it('should keep calling the endpoint after a transient failure', async () => { + postSpy.and.returnValue(throwError(() => ({ status: 500 }))); + expect(await taskCounters()).toEqual({ counters: {}, batched: false }); + + postSpy.and.returnValue(of(countersMock)); + expect(await taskCounters()).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true }); + expect(postSpy).toHaveBeenCalledTimes(2); + }); + }); + }); + + describe('batched counters opted in by the filter components', () => { + it('should not call the batched count endpoint when it was not asked for', async () => { + const result = await firstValueFrom(service.getFilterCounters('mock-app', FilterCounterEntityType.TASK)); + + expect(result).toEqual({ counters: {}, batched: false }); + expect(postSpy).not.toHaveBeenCalled(); + }); + + it('should not load the filters when the batched count endpoint was not asked for', async () => { + await firstValueFrom(service.getFilterCounters('mock-app', FilterCounterEntityType.TASK)); + + expect(getTaskListFiltersSpy).not.toHaveBeenCalled(); + }); + + it('should call the batched count endpoint when every entity type on screen asked for it', async () => { + await bothCounters(); + + expect(postSpy).toHaveBeenCalledTimes(1); + }); + + it('should not call the batched count endpoint when one entity type on screen did not ask for it', fakeAsync(() => { + const results: FilterCountersResult[] = []; + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe((result) => results.push(result)); + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, false).subscribe(); + tick(0); + + expect(postSpy).not.toHaveBeenCalled(); + expect(results).toEqual([{ counters: {}, batched: false }]); + })); + + it('should call the batched count endpoint once the entity type that opted out leaves the screen', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + const processSubscription = service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, false).subscribe(); + tick(0); + + processSubscription.unsubscribe(); + service.refreshFilterCounters('mock-app'); + tick(0); + + expect(postSpy).toHaveBeenCalledTimes(1); + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK]); + })); + }); + + describe('counters scoped to the entity types on screen', () => { + it('should send the queries of the entity type on screen alone', async () => { + await taskCounters(); + + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK]); + }); + + it('should not load the filters of an entity type that is not on screen', async () => { + await taskCounters(); + + expect(getTaskListFiltersSpy).toHaveBeenCalled(); + expect(getProcessFiltersSpy).not.toHaveBeenCalled(); + }); + + it('should send the queries of both entity types when both are on screen', async () => { + await bothCounters(); + + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK, FilterCounterEntityType.PROCESS_INSTANCE]); + }); + + it('should resolve the counters again when an entity type joins the ones on screen', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK]); + + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe(); + tick(0); + + expect(postSpy).toHaveBeenCalledTimes(2); + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK, FilterCounterEntityType.PROCESS_INSTANCE]); + })); + + it('should stop covering an entity type once its counters hold no subscriber', fakeAsync(() => { + const taskSubscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe(); + tick(0); + + taskSubscription.unsubscribe(); + service.refreshFilterCounters('mock-app'); + tick(0); + + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.PROCESS_INSTANCE]); + })); + }); + + describe('teardown', () => { + it('should close the engine event subscription once the counters hold no subscriber', fakeAsync(() => { + const subscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1); + + subscription.unsubscribe(); + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2); + })); + + it('should keep the engine event subscription while another subscriber holds the same entity type', fakeAsync(() => { + const subscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + + subscription.unsubscribe(); + emitTaskEvent(); + tick(3000); + + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1); + expect(postSpy).toHaveBeenCalledTimes(2); + })); + + it('should release the filters subscription once nothing reads them', fakeAsync(() => { + const filters$ = new BehaviorSubject(taskFiltersMock); + getTaskListFiltersSpy.and.returnValue(filters$.asObservable()); + + const subscriptions = [ + service.getTaskFilters('mock-app').subscribe(), + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe() + ]; + tick(0); + expect(filters$.observed).toBeTrue(); + + subscriptions.forEach((subscription) => subscription.unsubscribe()); + + expect(filters$.observed).toBeFalse(); + })); + + it('should resolve the counters again for a subscriber that comes after a full teardown', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe().unsubscribe(); + tick(0); + postSpy.calls.reset(); + + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + + expect(postSpy).toHaveBeenCalledTimes(1); + })); + }); + + describe('refreshFilterCounters', () => { + it('should resolve the counters again with a single request', fakeAsync(() => { + const results: FilterCountersResult[] = []; + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe((result) => results.push(result)); + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe(); + tick(0); + + service.refreshFilterCounters('mock-app'); + tick(0); + + expect(postSpy).toHaveBeenCalledTimes(2); + expect(results.length).toBe(2); + })); + + it('should not resolve the counters of an app without subscribers', fakeAsync(() => { + service.refreshFilterCounters('mock-app'); + tick(0); + + expect(postSpy).not.toHaveBeenCalled(); + })); + }); + + describe('when only one of the two filter families is wired', () => { + const configureTasksOnly = () => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [NoopAuthModule, ApolloTestingModule], + providers: [{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }] + }); + + const tasksOnlyService = TestBed.inject(FilterCountersCloudService); + TestBed.inject(AppConfigService).config.bpmHost = 'https://fake-bpm-host.com'; + spyOn(TestBed.inject(NotificationCloudService), 'makeGQLQuery').and.returnValue(new Subject().asObservable()); + spyOn(TestBed.inject(TaskFilterCloudService), 'getTaskListFilters').and.returnValue(of(taskFiltersMock)); + postSpy = spyOn(tasksOnlyService as unknown as CountEndpoint, 'post').and.returnValue(of(countersMock)); + + return tasksOnlyService; + }; + + it('should resolve the counters of the wired family', async () => { + const tasksOnlyService = configureTasksOnly(); + + const result = await firstValueFrom(tasksOnlyService.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true)); + + expect(result).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true }); + }); + + it('should leave the filters of the family that is not wired out of the request', async () => { + const tasksOnlyService = configureTasksOnly(); + + await firstValueFrom(tasksOnlyService.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true)); + + expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']); + expect(countRequest().PROCESS_INSTANCE).toBeUndefined(); + }); + }); + + describe('getEngineEvents', () => { + it('should return EMPTY when appName is not set', () => { + let completed = false; + service.getEngineEvents('', FilterCounterEntityType.TASK).subscribe({ complete: () => (completed = true) }); + + expect(completed).toBeTrue(); + expect(makeGQLQuerySpy).not.toHaveBeenCalled(); + }); + + it('should subscribe to the events of the task entity type alone', () => { + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(); + + const [appName, query] = makeGQLQuerySpy.calls.mostRecent().args; + expect(appName).toBe('mock-app'); + expect(query).toContain('TASK_CREATED'); + expect(query).not.toContain('PROCESS_STARTED'); + }); + + it('should subscribe to the events of the process entity type alone', () => { + service.getEngineEvents('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe(); + + const [, query] = makeGQLQuerySpy.calls.mostRecent().args; + expect(query).toContain('PROCESS_STARTED'); + expect(query).not.toContain('TASK_CREATED'); + }); + + it('should open a single subscription for multiple subscribers of the same entity type', () => { + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(); + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(); + + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1); + }); + + it('should open a separate subscription per entity type', () => { + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(); + service.getEngineEvents('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe(); + + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2); + }); + + it('should open a separate subscription per app', () => { + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(); + service.getEngineEvents('other-app', FilterCounterEntityType.TASK).subscribe(); + + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2); + }); + + it('should emit the debounced batch of events', fakeAsync(() => { + const batches: TaskCloudEngineEvent[][] = []; + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe((events) => batches.push(events)); + + emitTaskEvent('TASK_CREATED'); + emitTaskEvent('TASK_ASSIGNED'); + tick(3000); + + expect(batches.length).toBe(1); + expect(batches[0][0].eventType).toBe('TASK_ASSIGNED'); + })); + + it('should debounce the events using the configured debounce time', fakeAsync(() => { + spyOnProperty(service, 'notificationDebounceTime', 'get').and.returnValue(5000); + let emitted = false; + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(() => (emitted = true)); + + emitTaskEvent(); + tick(3000); + expect(emitted).toBeFalse(); + + tick(2000); + expect(emitted).toBeTrue(); + })); + }); + + describe('counters driven by the engine events', () => { + it('should make a single count request for a batch of events of both entity types', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe(); + tick(0); + postSpy.calls.reset(); + + emitTaskEvent(); + emitProcessEvent(); + tick(3000); + + expect(postSpy).toHaveBeenCalledTimes(1); + })); + + it('should make a single count request for the events of both entity types arriving apart', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe(); + tick(0); + postSpy.calls.reset(); + + emitTaskEvent(); + tick(1000); + emitProcessEvent(); + tick(3000); + + expect(postSpy).toHaveBeenCalledTimes(1); + })); + + it('should emit the counters resolved for the batch of events', fakeAsync(() => { + const results: FilterCountersResult[] = []; + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe((result) => results.push(result)); + tick(0); + + postSpy.and.returnValue(of({ TASK: { 'my-tasks': 9 } })); + emitTaskEvent(); + tick(3000); + + expect(results.length).toBe(2); + expect(results[1]).toEqual({ counters: { 'my-tasks': 9 }, batched: true }); + })); + + it('should not subscribe to the events of an entity type that is not on screen', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1); + expect(makeGQLQuerySpy.calls.mostRecent().args[1]).toContain('TASK_CREATED'); + })); + + it('should not resolve the counters again on the events of an entity type that is not on screen', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + postSpy.calls.reset(); + + emitProcessEvent(); + tick(3000); + + expect(postSpy).not.toHaveBeenCalled(); + })); + + it('should stop resolving the counters on the events of an entity type that left the screen', fakeAsync(() => { + const taskSubscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe(); + tick(0); + + taskSubscription.unsubscribe(); + postSpy.calls.reset(); + emitTaskEvent(); + tick(3000); + + expect(postSpy).not.toHaveBeenCalled(); + })); + + it('should not subscribe to the engine events when notifications are disabled', fakeAsync(() => { + appConfigService.config.notifications = false; + + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(3000); + + expect(makeGQLQuerySpy).not.toHaveBeenCalled(); + expect(postSpy).toHaveBeenCalledTimes(1); + })); + }); +}); diff --git a/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.ts b/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.ts new file mode 100644 index 0000000000..0d0efb20dc --- /dev/null +++ b/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.ts @@ -0,0 +1,375 @@ +/*! + * @license + * Copyright Š 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { inject, Injectable, Injector } from '@angular/core'; +import { asapScheduler, combineLatest, defer, EMPTY, merge, Observable, of, Subject, Subscription } from 'rxjs'; +import { catchError, debounceTime, finalize, map, shareReplay, switchMap, take } from 'rxjs/operators'; +import { BaseCloudService } from './base-cloud.service'; +import { NotificationCloudService } from './notification-cloud.service'; +import { TaskCloudEngineEvent } from '../models/engine-event-cloud.model'; +import { TaskFilterCloudService } from '../task/task-filters/services/task-filter-cloud.service'; +import { ProcessFilterCloudService } from '../process/process-filters/services/process-filter-cloud.service'; +import { TaskListCloudService } from '../task/task-list/services/task-list-cloud.service'; +import { ProcessListCloudService } from '../process/process-list/services/process-list-cloud.service'; +import { TaskFilterCloudAdapter } from '../models/filter-cloud-model'; +import { TaskFilterCloudModel } from '../task/task-filters/models/filter-cloud.model'; +import { ProcessFilterCloudModel } from '../process/process-filters/models/process-filter-cloud.model'; +import { ProcessFilterCloudAdapter } from '../process/process-list/models/process-cloud-query-request.model'; +import { + FilterCounterCandidate, + FilterCounterEntityType, + FilterCounters, + FilterCountersQuery, + FilterCountersRequest, + FilterCountersResult +} from '../models/filter-counters-cloud.model'; +const BATCHED_COUNTERS_UNAVAILABLE_STATUSES = [404, 501]; + +interface FilterCountersFilters { + [FilterCounterEntityType.TASK]: TaskFilterCloudModel[]; + [FilterCounterEntityType.PROCESS_INSTANCE]: ProcessFilterCloudModel[]; +} + +interface EngineEventsData { + engineEvents?: TaskCloudEngineEvent[]; +} + +const ENGINE_EVENTS_SUBSCRIPTION_QUERIES: Record = { + [FilterCounterEntityType.TASK]: ` + subscription { + engineEvents(eventType: [ + TASK_COMPLETED + TASK_ASSIGNED + TASK_ACTIVATED + TASK_SUSPENDED + TASK_CANCELLED + TASK_CREATED + ]) { + eventType + entity + } + } +`, + [FilterCounterEntityType.PROCESS_INSTANCE]: ` + subscription { + engineEvents(eventType: [ + PROCESS_CANCELLED + PROCESS_COMPLETED + PROCESS_CREATED + PROCESS_RESUMED + PROCESS_SUSPENDED + PROCESS_STARTED + ]) { + eventType + entity + } + } +` +}; + +@Injectable({ providedIn: 'root' }) +export class FilterCountersCloudService extends BaseCloudService { + private readonly notificationCloudService = inject(NotificationCloudService); + private readonly taskListCloudService = inject(TaskListCloudService); + private readonly processListCloudService = inject(ProcessListCloudService); + private readonly injector = inject(Injector); + + private readonly eventsPerEntityType = new Map>(); + private readonly rawEventsPerEntityType = new Map>(); + private readonly recountPerApp = new Map>(); + private readonly eventRecountPerApp = new Map>(); + private readonly activeEntityTypesPerApp = new Map>(); + private readonly subscribersPerEntityType = new Map(); + private readonly batchedCountersPerEntityType = new Map(); + private readonly eventSubscriptionsPerEntityType = new Map(); + private readonly appsWithoutBatchedCounters = new Set(); + private readonly taskFiltersPerApp = new Map>(); + private readonly processFiltersPerApp = new Map>(); + private readonly countersPerApp = new Map>(); + + get notificationDebounceTime(): number { + return this.appConfigService.get('notificationDebounceTime', 3000); + } + + getTaskFilters(appName: string): Observable { + return this.shareFilters(this.taskFiltersPerApp, appName, () => this.injector.get(TaskFilterCloudService).getTaskListFilters(appName)); + } + + getProcessFilters(appName: string): Observable { + return this.shareFilters(this.processFiltersPerApp, appName, () => this.injector.get(ProcessFilterCloudService).getProcessFilters(appName)); + } + + getFilterCounters(appName: string, entityType: FilterCounterEntityType, batchedCounters = false): Observable { + if (!appName) { + return EMPTY; + } + + return defer(() => { + this.activateEntityType(appName, entityType, batchedCounters); + + return this.getCounters(appName); + }).pipe( + map(({ counters, batched }) => ({ counters: counters[entityType] ?? {}, batched })), + finalize(() => this.deactivateEntityType(appName, entityType)) + ); + } + + refreshFilterCounters(appName: string): void { + this.recount(appName); + } + + getEngineEvents(appName: string, entityType: FilterCounterEntityType): Observable { + if (!appName) { + return EMPTY; + } + + const key = this.entityTypeKey(appName, entityType); + let events$ = this.eventsPerEntityType.get(key); + if (!events$) { + events$ = this.rawEngineEvents(appName, entityType).pipe( + debounceTime(this.notificationDebounceTime), + shareReplay({ bufferSize: 1, refCount: true }) + ); + this.eventsPerEntityType.set(key, events$); + } + + return events$; + } + + private rawEngineEvents(appName: string, entityType: FilterCounterEntityType): Observable { + const key = this.entityTypeKey(appName, entityType); + let events$ = this.rawEventsPerEntityType.get(key); + if (!events$) { + events$ = defer(() => + this.notificationCloudService.makeGQLQuery(appName, ENGINE_EVENTS_SUBSCRIPTION_QUERIES[entityType]) + ).pipe( + map((result) => result.data?.engineEvents ?? []), + catchError(() => EMPTY), + shareReplay({ bufferSize: 1, refCount: true }) + ); + this.rawEventsPerEntityType.set(key, events$); + } + + return events$; + } + + private get notificationsEnabled(): boolean { + return this.appConfigService.get('notifications', true); + } + + private activateEntityType(appName: string, entityType: FilterCounterEntityType, batchedCounters: boolean): void { + const key = this.entityTypeKey(appName, entityType); + const subscribers = (this.subscribersPerEntityType.get(key) ?? 0) + 1; + this.subscribersPerEntityType.set(key, subscribers); + + if (subscribers > 1) { + return; + } + + this.batchedCountersPerEntityType.set(key, batchedCounters); + + const activeEntityTypes = this.activeEntityTypes(appName); + const joinsResolvedCounters = activeEntityTypes.size > 0; + activeEntityTypes.add(entityType); + + if (this.notificationsEnabled) { + this.eventSubscriptionsPerEntityType.set( + key, + this.rawEngineEvents(appName, entityType).subscribe(() => this.eventRecountTrigger(appName).next()) + ); + } + + if (joinsResolvedCounters) { + this.recount(appName); + } + } + + private deactivateEntityType(appName: string, entityType: FilterCounterEntityType): void { + const key = this.entityTypeKey(appName, entityType); + const subscribers = (this.subscribersPerEntityType.get(key) ?? 1) - 1; + + if (subscribers > 0) { + this.subscribersPerEntityType.set(key, subscribers); + return; + } + + this.subscribersPerEntityType.delete(key); + this.batchedCountersPerEntityType.delete(key); + this.activeEntityTypes(appName).delete(entityType); + this.eventSubscriptionsPerEntityType.get(key)?.unsubscribe(); + this.eventSubscriptionsPerEntityType.delete(key); + } + + private activeEntityTypes(appName: string): Set { + let activeEntityTypes = this.activeEntityTypesPerApp.get(appName); + if (!activeEntityTypes) { + activeEntityTypes = new Set(); + this.activeEntityTypesPerApp.set(appName, activeEntityTypes); + } + + return activeEntityTypes; + } + + private entityTypeKey(appName: string, entityType: FilterCounterEntityType): string { + return `${appName}|${entityType}`; + } + + private recount(appName: string): void { + this.recountTrigger(appName).next(); + } + + private getFiltersForCounters(appName: string): Observable { + const activeEntityTypes = this.activeEntityTypes(appName); + + return combineLatest({ + [FilterCounterEntityType.TASK]: activeEntityTypes.has(FilterCounterEntityType.TASK) + ? this.getTaskFilters(appName).pipe(catchError(() => of([]))) + : of([]), + [FilterCounterEntityType.PROCESS_INSTANCE]: activeEntityTypes.has(FilterCounterEntityType.PROCESS_INSTANCE) + ? this.getProcessFilters(appName).pipe(catchError(() => of([]))) + : of([]) + }); + } + + private shareFilters(cache: Map>, appName: string, loadFilters: () => Observable): Observable { + let filters$ = cache.get(appName); + if (!filters$) { + filters$ = defer(loadFilters).pipe(shareReplay({ bufferSize: 1, refCount: true })); + cache.set(appName, filters$); + } + + return filters$; + } + + private getCounters(appName: string): Observable<{ counters: FilterCounters; batched: boolean }> { + let counters$ = this.countersPerApp.get(appName); + if (!counters$) { + counters$ = this.recounts(appName).pipe( + switchMap(() => this.resolveCounters(appName)), + shareReplay({ bufferSize: 1, refCount: true }) + ); + this.countersPerApp.set(appName, counters$); + } + + return counters$; + } + + private resolveCounters(appName: string): Observable<{ counters: FilterCounters; batched: boolean }> { + if (!this.batchedCountersEnabled(appName) || this.appsWithoutBatchedCounters.has(appName)) { + return of({ counters: {}, batched: false }); + } + + return this.getFiltersForCounters(appName).pipe( + take(1), + switchMap((filters) => this.fetchFilterCounters(appName, this.buildRequest(filters))), + map((counters) => ({ counters, batched: true })), + catchError((error) => { + if (BATCHED_COUNTERS_UNAVAILABLE_STATUSES.includes(error?.status)) { + this.appsWithoutBatchedCounters.add(appName); + } + + return of({ counters: {}, batched: false }); + }) + ); + } + + private batchedCountersEnabled(appName: string): boolean { + const activeEntityTypes = [...this.activeEntityTypes(appName)]; + + return ( + activeEntityTypes.length > 0 && + activeEntityTypes.every((entityType) => this.batchedCountersPerEntityType.get(this.entityTypeKey(appName, entityType))) + ); + } + + private recounts(appName: string): Observable { + return merge( + merge(of(undefined), this.recountTrigger(appName)).pipe(debounceTime(0, asapScheduler)), + this.eventRecountTrigger(appName).pipe(debounceTime(this.notificationDebounceTime)) + ); + } + + private recountTrigger(appName: string): Subject { + let recount$ = this.recountPerApp.get(appName); + if (!recount$) { + recount$ = new Subject(); + this.recountPerApp.set(appName, recount$); + } + + return recount$; + } + + private eventRecountTrigger(appName: string): Subject { + let eventRecount$ = this.eventRecountPerApp.get(appName); + if (!eventRecount$) { + eventRecount$ = new Subject(); + this.eventRecountPerApp.set(appName, eventRecount$); + } + + return eventRecount$; + } + + private buildRequest(filters: FilterCountersFilters): FilterCountersRequest { + const request: FilterCountersRequest = {}; + + const taskQueries = this.buildQueries(filters[FilterCounterEntityType.TASK], (filter) => + this.taskListCloudService.buildQueryData(new TaskFilterCloudAdapter(filter)) + ); + if (taskQueries.length) { + request[FilterCounterEntityType.TASK] = taskQueries; + } + + const processQueries = this.buildQueries(filters[FilterCounterEntityType.PROCESS_INSTANCE], (filter) => + this.processListCloudService.buildQueryData(new ProcessFilterCloudAdapter(filter)) + ); + if (processQueries.length) { + request[FilterCounterEntityType.PROCESS_INSTANCE] = processQueries; + } + + return request; + } + + private buildQueries( + filters: T[], + buildQuery: (filter: T) => Omit + ): FilterCountersQuery[] { + return (filters ?? []) + .filter((filter) => filter?.showCounter && this.isCounterBatched(filter)) + .map((filter) => { + try { + return { ...buildQuery(filter), requestId: filter.key as string }; + } catch { + return undefined; + } + }) + .filter((query): query is FilterCountersQuery => !!query); + } + + private fetchFilterCounters(appName: string, request: FilterCountersRequest): Observable { + if (!Object.keys(request).length) { + return of({}); + } + + const queryUrl = `${this.getBasePath(appName)}/query/v1/count`; + + return this.post(queryUrl, request).pipe(map((counters) => counters || {})); + } + + private isCounterBatched(filter: FilterCounterCandidate): boolean { + return !!filter?.key; + } +} diff --git a/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts b/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts index ae0cfe68fc..7b5a46571a 100644 --- a/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts @@ -15,8 +15,9 @@ * limitations under the License. */ -import { gql } from '@apollo/client/core'; +import { FetchResult, gql } from '@apollo/client/core'; import { Injectable, inject } from '@angular/core'; +import { Observable } from 'rxjs'; import { WebSocketService } from './web-socket.service'; @Injectable({ providedIn: 'root' @@ -24,8 +25,8 @@ import { WebSocketService } from './web-socket.service'; export class NotificationCloudService { private readonly webSocketService = inject(WebSocketService); - makeGQLQuery(appName: string, gqlQuery: string) { - return this.webSocketService.getSubscription({ + makeGQLQuery(appName: string, gqlQuery: string): Observable> { + return this.webSocketService.getSubscription({ apolloClientName: appName, wsUrl: `${appName}/notifications`, httpUrl: `${appName}/notifications/v2/ws/graphql`, diff --git a/lib/process-services-cloud/src/lib/services/public-api.ts b/lib/process-services-cloud/src/lib/services/public-api.ts index e3ab7c3b05..e300e35a83 100644 --- a/lib/process-services-cloud/src/lib/services/public-api.ts +++ b/lib/process-services-cloud/src/lib/services/public-api.ts @@ -17,6 +17,7 @@ export * from './base-cloud.service'; export * from './cloud-token.service'; +export * from './filter-counters-cloud.service'; export * from './form-fields.interfaces'; export * from './local-preference-cloud.service'; export * from './notification-cloud.service'; diff --git a/lib/process-services-cloud/src/lib/services/web-socket.service.spec.ts b/lib/process-services-cloud/src/lib/services/web-socket.service.spec.ts index 9f47740424..402b1ae380 100644 --- a/lib/process-services-cloud/src/lib/services/web-socket.service.spec.ts +++ b/lib/process-services-cloud/src/lib/services/web-socket.service.spec.ts @@ -16,18 +16,39 @@ */ import { TestBed } from '@angular/core/testing'; +import { Injectable } from '@angular/core'; import { Apollo, gql } from 'apollo-angular'; import { lastValueFrom, of, Subject } from 'rxjs'; import { WebSocketService } from './web-socket.service'; -import { SubscriptionOptions } from '@apollo/client/core'; +import { ApolloLink, execute, FetchResult, Observable as ApolloObservable, SubscriptionOptions } from '@apollo/client/core'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { AuthenticationService, AppConfigService } from '@alfresco/adf-core'; +import { Client, ClientOptions, Sink, SubscribePayload } from 'graphql-ws'; +import { HttpLink } from 'apollo-angular/http'; + +@Injectable() +class TestWebSocketService extends WebSocketService { + public capturedOnError: (() => void) | undefined; + + protected override createWsClient(clientOptions: ClientOptions): Client { + this.capturedOnError = clientOptions.on?.error as (() => void) | undefined; + + return { + on: () => () => undefined, + subscribe: (_payload: SubscribePayload, _sink: Sink) => () => undefined, + async *iterate() {}, + terminate: () => undefined, + dispose: () => undefined + }; + } +} describe('WebSocketService', () => { - let service: WebSocketService; + let service: TestWebSocketService; const onLogoutSubject: Subject = new Subject(); - const apolloMock = jasmine.createSpyObj('Apollo', ['use', 'createNamed']); + const apolloMock = jasmine.createSpyObj('Apollo', ['use', 'createNamed', 'removeClient']); + const httpLinkMock = jasmine.createSpyObj('HttpLink', ['create']); beforeEach(() => { TestBed.configureTestingModule({ @@ -37,6 +58,14 @@ describe('WebSocketService', () => { provide: Apollo, useValue: apolloMock }, + { + provide: WebSocketService, + useClass: TestWebSocketService + }, + { + provide: HttpLink, + useValue: httpLinkMock + }, { provide: AppConfigService, useValue: { @@ -52,13 +81,15 @@ describe('WebSocketService', () => { } ] }); - service = TestBed.inject(WebSocketService); + service = TestBed.inject(WebSocketService) as TestWebSocketService; apolloMock.use.and.returnValues(undefined, { subscribe: () => of({}) }); }); afterEach(() => { apolloMock.use.calls.reset(); apolloMock.createNamed.calls.reset(); + apolloMock.removeClient.calls.reset(); + httpLinkMock.create.calls.reset(); }); it('should not create a new Apollo client if it is already in use', async () => { @@ -95,7 +126,7 @@ describe('WebSocketService', () => { const apolloClientName = 'testClient'; const subscriptionOptions: SubscriptionOptions = { query: gql(`subscription {testQuery}`) }; const wsOptions = { apolloClientName, wsUrl: 'testUrl', subscriptionOptions }; - apolloMock.createNamed.and.callFake((_, options) => { + apolloMock.createNamed.and.callFake((_: any, options: { headers: {} }) => { headers = options.headers; }); @@ -105,4 +136,74 @@ describe('WebSocketService', () => { expect(apolloMock.createNamed).toHaveBeenCalled(); expect(headers).toEqual(expectedHeaders); }); + + it('should recreate the subscription client when the websocket connection errors', async () => { + const apolloClientName = 'testClient'; + const subscriptionOptions: SubscriptionOptions = { query: gql(`subscription {testQuery}`) }; + const wsOptions = { apolloClientName, wsUrl: 'testUrl', subscriptionOptions }; + + await lastValueFrom(service.getSubscription(wsOptions)); + + expect(apolloMock.createNamed).toHaveBeenCalledTimes(1); + expect(apolloMock.removeClient).not.toHaveBeenCalled(); + + if (!service.capturedOnError) { + fail('Expected websocket error handler to be registered'); + return; + } + + service.capturedOnError(); + + expect(apolloMock.removeClient).toHaveBeenCalledWith(apolloClientName); + expect(apolloMock.createNamed).toHaveBeenCalledTimes(2); + expect(apolloMock.createNamed).toHaveBeenCalledWith(apolloClientName, jasmine.any(Object)); + }); + + it('should retry the operation when a GraphQL error is unauthenticated', async () => { + const apolloClientName = 'testClient'; + const subscriptionOptions: SubscriptionOptions = { query: gql(`subscription {testQuery}`) }; + const wsOptions = { apolloClientName, wsUrl: 'testUrl', httpUrl: 'testHttpUrl', subscriptionOptions }; + const expectedResult: FetchResult = { data: { retried: true } }; + let createdLink: ApolloLink | undefined; + let requestCount = 0; + + httpLinkMock.create.and.returnValue( + new ApolloLink( + () => + new ApolloObservable((observer) => { + requestCount++; + + if (requestCount === 1) { + observer.next({ + errors: [{ message: 'Unauthorized', extensions: { code: 'UNAUTHENTICATED' } }] + }); + } else { + observer.next(expectedResult); + } + + observer.complete(); + }) + ) + ); + apolloMock.createNamed.and.callFake((_clientName: any, options: { link: ApolloLink | undefined }) => { + createdLink = options.link; + }); + + await lastValueFrom(service.getSubscription(wsOptions)); + + if (!createdLink) { + fail('Expected Apollo link to be created'); + return; + } + + const result = await new Promise((resolve, reject) => { + execute(createdLink!, { query: gql(`query { testQuery }`) }).subscribe({ + next: resolve, + error: reject + }); + }); + + expect(requestCount).toBe(2); + expect(result).toEqual(expectedResult); + }); }); diff --git a/lib/process-services-cloud/src/lib/services/web-socket.service.ts b/lib/process-services-cloud/src/lib/services/web-socket.service.ts index 4b9ebf95a9..c9e0e36076 100644 --- a/lib/process-services-cloud/src/lib/services/web-socket.service.ts +++ b/lib/process-services-cloud/src/lib/services/web-socket.service.ts @@ -15,10 +15,9 @@ * limitations under the License. */ -import { createClient } from 'graphql-ws'; +import { Client, ClientOptions, createClient } from 'graphql-ws'; import { inject, Injectable } from '@angular/core'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; -import { WebSocketLink } from '@apollo/client/link/ws'; import { DefaultContext, FetchResult, @@ -56,9 +55,8 @@ export class WebSocketService { private readonly authService = inject(AuthenticationService); private readonly appConfigService = inject(AppConfigService); - private readonly subscriptionProtocol: 'graphql-ws' | 'transport-ws' = 'graphql-ws'; - private wsLink: GraphQLWsLink | WebSocketLink; - private httpLinkHandler: HttpLinkHandler; + private wsLink!: GraphQLWsLink; + private httpLinkHandler: HttpLinkHandler | undefined; public getSubscription(options: serviceOptions): Observable> { const { apolloClientName, subscriptionOptions } = options; @@ -110,8 +108,7 @@ export class WebSocketService { operation.setContext(({ headers }: DefaultContext) => ({ headers: { ...headers, - ...(this.subscriptionProtocol === 'graphql-ws' && { Authorization: `Bearer ${this.authService.getToken()}` }), - ...(this.subscriptionProtocol === 'transport-ws' && { 'X-Authorization': `Bearer ${this.authService.getToken()}` }) + Authorization: `Bearer ${this.authService.getToken()}` } })); return forward(operation); @@ -120,8 +117,8 @@ export class WebSocketService { const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => { if (graphQLErrors) { for (const error of graphQLErrors) { - if (error.extensions && error.extensions['code'] === 'UNAUTHENTICATED') { - authLink(operation, forward); + if (error.extensions?.['code'] === 'UNAUTHENTICATED') { + return authLink(operation, forward); } } } @@ -129,6 +126,8 @@ export class WebSocketService { if (networkError) { console.error(`[Network error]: ${networkError}`); } + + return undefined; }); const retryLink = new RetryLink({ @@ -145,8 +144,7 @@ export class WebSocketService { this.apollo.createNamed(options.apolloClientName, { headers: { - ...(this.subscriptionProtocol === 'graphql-ws' && { Authorization: `Bearer ${this.authService.getToken()}` }), - ...(this.subscriptionProtocol === 'transport-ws' && { 'X-Authorization': `Bearer ${this.authService.getToken()}` }) + Authorization: `Bearer ${this.authService.getToken()}` }, link: from([authLink, retryLink, errorLink, link]), cache: new InMemoryCache({ merge: true } as InMemoryCacheConfig) @@ -155,22 +153,28 @@ export class WebSocketService { private createGraphQLWsLink(options: serviceOptions): void { this.wsLink = new GraphQLWsLink( - createClient({ + this.createWsClient({ url: this.createWsUrl(options.wsUrl) + '/v2/ws/graphql', connectionParams: () => ({ Authorization: 'Bearer ' + this.authService.getToken() }), on: { - error: () => { - this.apollo.removeClient(options.apolloClientName); - this.initSubscriptions(options); - } + error: () => this.reconnect(options) }, lazy: true }) ); } + protected createWsClient(clientOptions: ClientOptions): Client { + return createClient(clientOptions); + } + + private reconnect(options: serviceOptions): void { + this.apollo.removeClient(options.apolloClientName); + this.initSubscriptions(options); + } + private createHttpLinkHandler(options: serviceOptions): void { this.httpLinkHandler = options.httpUrl ? this.httpLink.create({ diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.spec.ts index 5fb1f21daf..6fda86d7a0 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.spec.ts @@ -17,10 +17,10 @@ import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core'; import { Component, SimpleChange } from '@angular/core'; -import { ComponentFixture, TestBed, fakeAsync, flush, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, flush } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { first, of, Subject, throwError } from 'rxjs'; -import { TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service'; +import { first, NEVER, of, Subject, throwError } from 'rxjs'; +import { PROCESS_FILTERS_SERVICE_TOKEN, TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service'; import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service'; import { defaultTaskFiltersMock, fakeGlobalFilter, taskNotifications } from '../../mock/task-filters-cloud.mock'; import { TaskFilterCloudService } from '../../services/task-filter-cloud.service'; @@ -35,6 +35,9 @@ import { TaskFilterCloudModel } from '../../models/filter-cloud.model'; import { MatIconHarness } from '@angular/material/icon/testing'; import { ActivatedRoute, provideRouter, Router } from '@angular/router'; import { RouterTestingHarness } from '@angular/router/testing'; +import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service'; +import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model'; +import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model'; @Component({ selector: 'adf-cloud-dummy', template: '' }) class DummyComponent {} @@ -50,7 +53,10 @@ describe('TaskFiltersCloudComponent', () => { let getTaskFilterCounterSpy: jasmine.Spy; let getTaskListFiltersSpy: jasmine.Spy; let getTaskListCountSpy: jasmine.Spy; - let getTaskNotificationSubscriptionSpy: jasmine.Spy; + let getEngineEventsSpy: jasmine.Spy; + let filterCountersService: FilterCountersCloudService; + let getFilterCountersSpy: jasmine.Spy; + let refreshFilterCountersSpy: jasmine.Spy; let router: Router; const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => { @@ -58,6 +64,7 @@ describe('TaskFiltersCloudComponent', () => { imports: [NoopAuthModule, TaskFiltersCloudComponent, ApolloTestingModule], providers: [ { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, + { provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, provideRouter([{ path: 'task-list-cloud', component: DummyComponent }]), { provide: ActivatedRoute, @@ -76,10 +83,15 @@ describe('TaskFiltersCloudComponent', () => { }); taskFilterService = TestBed.inject(TaskFilterCloudService); taskListService = TestBed.inject(TaskListCloudService); + filterCountersService = TestBed.inject(FilterCountersCloudService); getTaskFilterCounterSpy = spyOn(taskFilterService, 'getTaskFilterCounter').and.returnValue(of(11)); getTaskListCountSpy = spyOn(taskListService, 'getTaskListCount').and.returnValue(of(11)); - getTaskNotificationSubscriptionSpy = spyOn(taskFilterService, 'getTaskNotificationSubscription').and.returnValue(of(taskNotifications)); - getTaskListFiltersSpy = spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(of(fakeGlobalFilter)); + getEngineEventsSpy = spyOn(filterCountersService, 'getEngineEvents').and.returnValue(of(taskNotifications)); + getTaskListFiltersSpy = spyOn(filterCountersService, 'getTaskFilters').and.returnValue(of(fakeGlobalFilter)); + getFilterCountersSpy = spyOn(filterCountersService, 'getFilterCounters').and.returnValue( + of({ counters: { 'fake-involved-tasks': 11 }, batched: true }) + ); + refreshFilterCountersSpy = spyOn(filterCountersService, 'refreshFilterCounters'); appConfigService = TestBed.inject(AppConfigService); @@ -261,7 +273,7 @@ describe('TaskFiltersCloudComponent', () => { expect(updatedFilterCounters.length).toBe(0); }); - it('should update filter counter when filter is selected', async () => { + it('should refresh the filter counters when a filter is selected', async () => { component.showIcons = true; await bindAppName(); @@ -269,7 +281,7 @@ describe('TaskFiltersCloudComponent', () => { filterButton.click(); fixture.detectChanges(); - expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(fakeGlobalFilter[0]); + expect(refreshFilterCountersSpy).toHaveBeenCalledWith('my-app-1'); }); describe('Notifications config', () => { @@ -306,30 +318,33 @@ describe('TaskFiltersCloudComponent', () => { }); it('should not subscribe to notifications when appName is missing', () => { - getTaskNotificationSubscriptionSpy.calls.reset(); + getEngineEventsSpy.calls.reset(); component.appName = ''; fixture.detectChanges(); - expect(getTaskNotificationSubscriptionSpy).not.toHaveBeenCalled(); + expect(getEngineEventsSpy).not.toHaveBeenCalled(); }); - it('should debounce notification subscription using the configured debounce time', fakeAsync(() => { - const notifications$ = new Subject(); - getTaskNotificationSubscriptionSpy.and.returnValue(notifications$.asObservable()); + it('should subscribe to the notifications of the bound app', () => { component.appName = 'my-app-1'; fixture.detectChanges(); - const updateFilterCountersSpy = spyOn(component, 'updateFilterCounters'); + expect(getEngineEventsSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK); + }); - notifications$.next(taskNotifications); - tick(1000); - expect(updateFilterCountersSpy).not.toHaveBeenCalled(); + it('should emit the events of the debounced batch', fakeAsync(() => { + const events$ = new Subject(); + getEngineEventsSpy.and.returnValue(events$.asObservable()); + const filterCounterUpdatedSpy = spyOn(component.filterCounterUpdated, 'emit'); + component.appName = 'my-app-1'; - tick(2000); - expect(updateFilterCountersSpy).toHaveBeenCalledTimes(1); + fixture.detectChanges(); + events$.next(taskNotifications); + + expect(filterCounterUpdatedSpy).toHaveBeenCalledWith(taskNotifications); flush(); })); }); @@ -438,7 +453,7 @@ describe('TaskFiltersCloudComponent', () => { expect(updatedFilterCounters.length).toBe(0); }); - it('should update filter counter when filter is selected', async () => { + it('should refresh the filter counters when a filter is selected', async () => { await bindAppName(); const filterButton = await loader.getHarness( @@ -446,6 +461,14 @@ describe('TaskFiltersCloudComponent', () => { ); await filterButton.click(); + expect(refreshFilterCountersSpy).toHaveBeenCalledWith('my-app-1'); + }); + + it('should resolve the counters with the POST method when the batched endpoint is not available', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false })); + + await bindAppName(); + expect(getTaskListCountSpy).toHaveBeenCalledWith(new TaskFilterCloudAdapter(fakeGlobalFilter[0])); }); }); @@ -658,17 +681,137 @@ describe('TaskFiltersCloudComponent', () => { expect(component.updatedCountersSet.has(fakeFilterKey)).toBe(true); }); - it('should call fetchTaskFilterCounter only if filter.showCounter is true', () => { + it('should resolve the counter only of the filters with a counter enabled', () => { const filterWithCounter = new TaskFilterCloudModel({ ...defaultTaskFiltersMock[0], showCounter: true }); const filterWithoutCounter = new TaskFilterCloudModel({ ...defaultTaskFiltersMock[1], showCounter: false }); - const fetchSpy = spyOn(component, 'fetchTaskFilterCounter').and.returnValue(of(42)); + getTaskFilterCounterSpy.calls.reset(); component.filters = [filterWithCounter, filterWithoutCounter]; component.updateFilterCounters(); - expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(fetchSpy).toHaveBeenCalledWith(filterWithCounter); - expect(fetchSpy).not.toHaveBeenCalledWith(filterWithoutCounter); + expect(getTaskFilterCounterSpy).toHaveBeenCalledTimes(1); + expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(filterWithCounter); + }); + + describe('Batched counters', () => { + it('should read the counters without waiting for the filters', async () => { + getTaskListFiltersSpy.and.returnValue(NEVER); + + await bindAppName(); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, false); + }); + + it('should hold the counters until the filters they belong to arrive', async () => { + const filters$ = new Subject(); + getTaskListFiltersSpy.and.returnValue(filters$.asObservable()); + getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 9 }, batched: true })); + + await bindAppName(); + expect(component.counters['fake-involved-tasks']).toBeUndefined(); + + filters$.next(fakeGlobalFilter); + fixture.detectChanges(); + + expect(component.counters['fake-involved-tasks']).toBe(9); + }); + + it('should read the counters of the task filters of the bound app', async () => { + await bindAppName(); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, false); + }); + + it('should not ask for the batched count endpoint by default', async () => { + await bindAppName(); + + expect(component.useBatchedCounters).toBeFalse(); + expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, false); + }); + + it('should ask for the batched count endpoint when the input is set', async () => { + fixture.componentRef.setInput('useBatchedCounters', true); + + await bindAppName(); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, true); + }); + + it('should read the counters again when the input changes', async () => { + await bindAppName(); + getFilterCountersSpy.calls.reset(); + + fixture.componentRef.setInput('useBatchedCounters', true); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, true); + }); + + it('should hold the counters resolved by the batched count request', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 9 }, batched: true })); + + await bindAppName(); + + expect(component.counters['fake-involved-tasks']).toBe(9); + }); + + it('should emit the filters whose counter changed', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 9 }, batched: true })); + const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); + + await bindAppName(); + + expect(updatedFilterSpy).toHaveBeenCalledWith('fake-involved-tasks'); + }); + + it('should resolve the counter of a filter the batch left out on its own', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: true })); + + await bindAppName(); + + expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(fakeGlobalFilter[0]); + expect(component.counters['fake-involved-tasks']).toBe(11); + }); + + it('should keep the counters of the other filters when one counter cannot be resolved', async () => { + getTaskListFiltersSpy.and.returnValue(of([fakeGlobalFilter[0], { ...fakeGlobalFilter[1], showCounter: true }])); + getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 4 }, batched: true })); + getTaskFilterCounterSpy.and.throwError('the query of the filter cannot be built'); + + await bindAppName(); + + expect(component.counters['fake-involved-tasks']).toBe(4); + expect(component.counters['fake-my-task1']).toBe(0); + }); + + it('should resolve the counters one filter at a time when the batched endpoint is not available', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false })); + + await bindAppName(); + + expect(getTaskFilterCounterSpy).toHaveBeenCalled(); + expect(component.counters['fake-involved-tasks']).toBe(11); + }); + + it('should refresh the counters of every filter when a filter is clicked', async () => { + await bindAppName(); + + component.onFilterClick(fakeGlobalFilter[0]); + + expect(refreshFilterCountersSpy).toHaveBeenCalledWith('my-app-1'); + }); + + it('should refresh the counter of the clicked filter alone when the batched endpoint is not available', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false })); + await bindAppName(); + getTaskFilterCounterSpy.calls.reset(); + + component.onFilterClick(fakeGlobalFilter[0]); + + expect(refreshFilterCountersSpy).not.toHaveBeenCalled(); + expect(getTaskFilterCounterSpy).toHaveBeenCalledTimes(1); + }); }); describe('Highlight Selected Filter', () => { diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.ts b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.ts index 3c553aa7c2..54f2891877 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.ts @@ -16,16 +16,18 @@ */ import { Component, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; -import { EMPTY, Observable } from 'rxjs'; +import { combineLatest, defer, EMPTY, Observable, of, Subscription } from 'rxjs'; import { TaskFilterCloudService } from '../../services/task-filter-cloud.service'; import { FilterParamsModel, TaskFilterCloudModel } from '../../models/filter-cloud.model'; import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core'; -import { catchError, debounceTime, map, shareReplay, tap } from 'rxjs/operators'; +import { catchError, map } from 'rxjs/operators'; import { BaseTaskFiltersCloudComponent } from '../base-task-filters-cloud.component'; import { TaskDetailsCloudModel } from '../../../models/task-details-cloud.model'; import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model'; import { TaskListCloudService } from '../../../task-list/services/task-list-cloud.service'; import { TaskFilterCloudAdapter } from '../../../../models/filter-cloud-model'; +import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service'; +import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { TranslatePipe } from '@ngx-translate/core'; @@ -42,10 +44,21 @@ import { AsyncPipe } from '@angular/common'; export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent implements OnInit, OnChanges { protected readonly TASKS_ROUTE = '/task-list-cloud'; - /** (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the task count. */ + /** + * (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the task count. + * + */ @Input() searchApiMethod: 'GET' | 'POST' = 'GET'; + /** + * (optional) Resolves the counters of the task and the process filters with a single call to + * `POST /query/v1/count`. Both filter components have to + * ask for it, otherwise the counters are resolved one filter at a time. + */ + @Input() + useBatchedCounters = false; + /** Emitted when a filter is being selected based on the filterParam input. */ @Output() filterSelected = new EventEmitter(); @@ -69,9 +82,13 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp notificationDebounceTime = 3000; currentFiltersValues: { [key: string]: number } = {}; private filtersLoadedFor?: string; + private countersSubscription?: Subscription; + private countersFilters$?: Observable; + private batchedCounters = true; private readonly taskFilterCloudService = inject(TaskFilterCloudService); private readonly taskListCloudService = inject(TaskListCloudService); + private readonly filterCountersCloudService = inject(FilterCountersCloudService); private readonly translationService = inject(TranslationService); private readonly appConfigService = inject(AppConfigService); private readonly activatedRoute = inject(ActivatedRoute); @@ -80,6 +97,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp ngOnInit() { this.enableNotifications = this.appConfigService.get('notifications', true); this.notificationDebounceTime = this.appConfigService.get('notificationDebounceTime', 3000); + if (!this.filtersLoadedFor) { this.getFilters(this.appName); } @@ -94,6 +112,8 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp this.getFilters(appName.currentValue); } else if (filter && filter.currentValue !== filter.previousValue) { this.selectFilterAndEmit(filter.currentValue); + } else if (changes['useBatchedCounters'] && !changes['useBatchedCounters'].firstChange && this.filtersLoadedFor) { + this.loadFilterCounters(this.filtersLoadedFor); } } @@ -104,8 +124,8 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp */ getFilters(appName: string): void { this.filtersLoadedFor = appName; - const filters$ = this.taskFilterCloudService.getTaskListFilters(appName).pipe(shareReplay({ bufferSize: 1, refCount: true })); - this.filters$ = filters$.pipe(catchError(() => EMPTY)); + const filters$ = this.filterCountersCloudService.getTaskFilters(appName); + this.filters$ = filters$.pipe(catchError(() => of([]))); filters$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (res) => { @@ -113,13 +133,15 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp this.filters = res || []; this.initFilterCounters(); this.selectFilterAndEmit(this.filterParam); - this.updateFilterCounters(); this.success.emit(res); }, error: (err) => { this.error.emit(err); } }); + + this.countersFilters$ = filters$; + this.loadFilterCounters(appName); } /** @@ -131,55 +153,47 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp /** * Iterate over filters and update counters + * + * @deprecated counts one filter at a time. Removed in ADF 10.0.0. */ updateFilterCounters(): void { - this.filters.forEach((filter: TaskFilterCloudModel) => this.updateFilterCounter(filter)); + this.filters.forEach((filter) => this.updateFilterCounter(filter)); } /** * Get current value for filter and check if value has changed * * @param filter filter + * @deprecated counts one filter at a time. Removed in ADF 10.0.0. */ updateFilterCounter(filter: TaskFilterCloudModel): void { if (!filter?.showCounter) { return; } - this.fetchTaskFilterCounter(filter) + + defer(() => this.fetchTaskFilterCounter(filter)) .pipe( - tap((filterCounter) => { - this.checkIfFilterValuesHasBeenUpdated(filter.key, filterCounter); - }) + catchError(() => EMPTY), + takeUntilDestroyed(this.destroyRef) ) - .subscribe((data) => { - this.counters = { - ...this.counters, - [filter.key]: data - }; + .subscribe((counter) => { + this.checkIfFilterValuesHasBeenUpdated(filter.key, counter); + this.counters = { ...this.counters, [filter.key]: counter }; }); } - private fetchTaskFilterCounter(filter: TaskFilterCloudModel): Observable { - return this.searchApiMethod === 'POST' - ? this.taskListCloudService.getTaskListCount(new TaskFilterCloudAdapter(filter)) - : this.taskFilterCloudService.getTaskFilterCounter(filter); - } - - initFilterCounterNotifications() { + initFilterCounterNotifications(): void { if (!this.appName) { return; } - if (this.enableNotifications) { - this.taskFilterCloudService - .getTaskNotificationSubscription(this.appName) - .pipe(debounceTime(this.notificationDebounceTime), takeUntilDestroyed(this.destroyRef)) - .subscribe((result) => { - result.forEach((taskEvent) => { - this.checkFilterCounter(taskEvent.entity); - }); - this.updateFilterCounters(); - this.filterCounterUpdated.emit(result); + if (this.enableNotifications) { + this.filterCountersCloudService + .getEngineEvents(this.appName, FilterCounterEntityType.TASK) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((events) => { + events.forEach((taskEvent) => this.checkFilterCounter(taskEvent.entity)); + this.filterCounterUpdated.emit(events); }); } else { this.counters = {}; @@ -240,7 +254,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp onFilterClick(filter: FilterParamsModel) { if (filter) { this.selectFilter(filter); - this.updateFilterCounter(this.currentFilter); + this.refreshFilterCounter(this.currentFilter); this.filterClicked.emit(this.currentFilter); this.updatedCountersSet.delete(filter.key); } else { @@ -267,17 +281,9 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp return this.filters === undefined || (this.filters && this.filters.length === 0); } - /** - * Reset the filters properties - */ - private resetFilter() { - this.filters = []; - this.currentFilter = undefined; - } - checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number) { if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) { - this.currentFiltersValues[filterKey] = filterValue; + this.currentFiltersValues = { ...this.currentFiltersValues, [filterKey]: filterValue }; this.updatedFilter.emit(filterKey); this.updatedCountersSet.add(filterKey); } @@ -288,8 +294,69 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp * */ getFilterKeysAfterExternalRefreshing(): void { - this.taskFilterCloudService.filterKeyToBeRefreshed$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((filterKey: string) => { - this.updatedCountersSet.delete(filterKey); + this.taskFilterCloudService.filterKeyToBeRefreshed$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((filterKey: string) => this.updatedCountersSet.delete(filterKey)); + } + + private loadFilterCounters(appName: string): void { + if (!this.countersFilters$) { + return; + } + + this.countersSubscription?.unsubscribe(); + this.countersSubscription = combineLatest([ + this.countersFilters$.pipe(catchError(() => of([]))), + this.filterCountersCloudService.getFilterCounters(appName, FilterCounterEntityType.TASK, this.useBatchedCounters) + ]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(([, { counters, batched }]) => { + this.batchedCounters = batched; + if (batched) { + this.applyFilterCounters(counters); + } else { + this.updateFilterCounters(); + } + }); + } + + private applyFilterCounters(counters: { [filterKey: string]: number }): void { + this.filters.forEach((filter) => { + const filterKey = filter?.showCounter ? filter.key : undefined; + if (!filterKey) { + return; + } + + const counter = counters[filterKey]; + if (counter === undefined) { + this.updateFilterCounter(filter); + return; + } + + this.checkIfFilterValuesHasBeenUpdated(filterKey, counter); + this.counters = { ...this.counters, [filterKey]: counter }; }); } + + private fetchTaskFilterCounter(filter: TaskFilterCloudModel): Observable { + return this.searchApiMethod === 'POST' + ? this.taskListCloudService.getTaskListCount(new TaskFilterCloudAdapter(filter)) + : this.taskFilterCloudService.getTaskFilterCounter(filter); + } + + /** + * Reset the filters properties + */ + private resetFilter() { + this.filters = []; + this.currentFilter = undefined; + } + + private refreshFilterCounter(filter: TaskFilterCloudModel): void { + if (this.batchedCounters) { + this.filterCountersCloudService.refreshFilterCounters(this.appName); + } else { + this.updateFilterCounter(filter); + } + } } diff --git a/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.ts b/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.ts index 0018f4231f..e8a38a776b 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.ts @@ -361,6 +361,11 @@ export class TaskFilterCloudService extends BaseCloudService { ]; } + /** + * @deprecated use FilterCountersCloudService.getEngineEvents instead. + * @param appName Name of the target app + * @returns Task engine events + */ getTaskNotificationSubscription(appName: string): Observable { return this.notificationCloudService .makeGQLQuery(appName, TASK_EVENT_SUBSCRIPTION_QUERY) diff --git a/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.ts b/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.ts index 7d6f6ce04c..c46f7f7ef0 100644 --- a/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.ts @@ -135,7 +135,7 @@ export class TaskListCloudService extends BaseCloudService implements TaskListCl return this.post(queryUrl, queryData).pipe(map((response) => response || 0)); } - protected buildQueryData(requestNode: TaskListRequestModel) { + buildQueryData(requestNode: TaskListRequestModel) { const queryData: any = { id: requestNode.id, parentId: requestNode.parentId, diff --git a/lib/process-services-cloud/src/public-api.ts b/lib/process-services-cloud/src/public-api.ts index fa63874f47..b945932abf 100644 --- a/lib/process-services-cloud/src/public-api.ts +++ b/lib/process-services-cloud/src/public-api.ts @@ -33,6 +33,7 @@ export * from './lib/models/application-version.model'; export * from './lib/models/engine-event-cloud.model'; export * from './lib/models/task-cloud.model'; export * from './lib/models/filter-cloud-model'; +export * from './lib/models/filter-counters-cloud.model'; export * from './lib/models/task-list-sorting.model'; export * from './lib/models/process-instance-variable.model'; export * from './lib/models/variable-definition'; diff --git a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.html b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.html index 55a0f27e0c..744843e374 100644 --- a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.html +++ b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.html @@ -6,17 +6,18 @@ - + {{opt.name}} {{field.value}} @if (!isReadOnlyField && dropdownControl.touched && (field.validationSummary?.message || dropdownControl.hasError('required'))) { - + error_outline @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}} } + diff --git a/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.html b/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.html index c4160a0040..8cea85eaa1 100644 --- a/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.html +++ b/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.html @@ -4,7 +4,7 @@ [class.adf-invalid]="!field.isValid" [class.adf-readonly]="field.readOnly" id="typehead-div"> - + @if (field.validationSummary?.message || isInvalidFieldRequired()) { - + error_outline @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}} } + diff --git a/package.json b/package.json index 790f5677b8..5046799998 100644 --- a/package.json +++ b/package.json @@ -52,12 +52,12 @@ "@angular/platform-browser-dynamic": "20.3.27", "@angular/router": "20.3.27", "@apollo/client": "3.13.1", - "@cspell/eslint-plugin": "10.0.0", + "@cspell/eslint-plugin": "10.0.1", "@mat-datetimepicker/core": "16.0.1", "@ngx-translate/core": "17.0.0", "angular-oauth2-oidc": "19.0.0", "apollo-angular": "11.0.0", - "chart.js": "4.4.4", + "chart.js": "4.5.1", "cropperjs": "1.6.2", "date-fns": "2.30.0", "dotenv-expand": "5.1.0", @@ -66,7 +66,7 @@ "graphql-ws": "6.0.8", "material-icons": "1.13.14", "minimatch": "10.2.6", - "ng2-charts": "4.1.1", + "ng2-charts": "9.0.0", "pdfjs-dist": "5.1.91", "raphael": "2.3.0", "rxjs": "7.8.2", @@ -98,10 +98,10 @@ "@types/jasminewd2": "2.0.13", "@types/node": "26.1.1", "@types/sinon": "22.0.0", - "@typescript-eslint/eslint-plugin": "8.67.0", - "@typescript-eslint/parser": "8.67.0", - "@typescript-eslint/typescript-estree": "8.67.0", - "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/eslint-plugin": "8.69.0", + "@typescript-eslint/parser": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0", "ajv": "8.20.0", "dotenv": "16.4.7", "eslint": "10.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e695136426..503833c718 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,7 @@ overrides: brace-expansion@5.0.8: 5.0.9 brace-expansion@<1.1.18: 1.1.18 socket.io-parser@4.2.6: 4.2.7 + browserslist@>=4.28.0 <4.28.8: 4.28.8 svgo: 4.0.2 shell-quote: 1.9.0 adm-zip: 0.6.0 @@ -31,7 +32,7 @@ importers: dependencies: '@angular-eslint/utils': specifier: 20.7.0 - version: 20.7.0(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) + version: 20.7.0(@typescript-eslint/utils@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) '@angular/animations': specifier: 20.3.27 version: 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) @@ -69,8 +70,8 @@ importers: specifier: 3.13.1 version: 3.13.1(graphql-ws@6.0.8(graphql@16.14.0)(ws@8.21.0))(graphql@16.14.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@cspell/eslint-plugin': - specifier: 10.0.0 - version: 10.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) + specifier: 10.0.1 + version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) '@mat-datetimepicker/core': specifier: 16.0.1 version: 16.0.1(@angular/cdk@20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/material@20.2.14(7fc7dc26a5364b20a95267fc7b4f10c7)) @@ -84,8 +85,8 @@ importers: specifier: 11.0.0 version: 11.0.0(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@apollo/client@3.13.1(graphql-ws@6.0.8(graphql@16.14.0)(ws@8.21.0))(graphql@16.14.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(graphql@16.14.0)(rxjs@7.8.2) chart.js: - specifier: 4.4.4 - version: 4.4.4 + specifier: 4.5.1 + version: 4.5.1 cropperjs: specifier: 1.6.2 version: 1.6.2 @@ -111,8 +112,8 @@ importers: specifier: 10.2.6 version: 10.2.6 ng2-charts: - specifier: 4.1.1 - version: 4.1.1(@angular/cdk@20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(chart.js@4.4.4)(rxjs@7.8.2) + specifier: 9.0.0 + version: 9.0.0(@angular/cdk@20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(chart.js@4.5.1)(rxjs@7.8.2) pdfjs-dist: specifier: 5.1.91 version: 5.1.91 @@ -146,10 +147,10 @@ importers: version: 20.3.32(chokidar@4.0.3) '@angular-eslint/eslint-plugin': specifier: 20.7.0 - version: 20.7.0(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) + version: 20.7.0(@typescript-eslint/utils@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) '@angular-eslint/eslint-plugin-template': specifier: 20.7.0 - version: 20.7.0(@angular-eslint/template-parser@22.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3))(@typescript-eslint/types@8.67.0)(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) + version: 20.7.0(@angular-eslint/template-parser@22.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3))(@typescript-eslint/types@8.69.0)(@typescript-eslint/utils@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) '@angular-eslint/template-parser': specifier: 22.1.0 version: 22.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) @@ -170,7 +171,7 @@ importers: version: 23.1.1(1871ac9779a3c1c87e7a2019dffef8ff) '@nx/eslint-plugin': specifier: 23.1.1 - version: 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@typescript-eslint/parser@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3) + version: 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@typescript-eslint/parser@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3) '@nx/js': specifier: 23.1.1 version: 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0) @@ -202,17 +203,17 @@ importers: specifier: 22.0.0 version: 22.0.0 '@typescript-eslint/eslint-plugin': - specifier: 8.67.0 - version: 8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + specifier: 8.69.0 + version: 8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) '@typescript-eslint/parser': - specifier: 8.67.0 - version: 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + specifier: 8.69.0 + version: 8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) '@typescript-eslint/typescript-estree': - specifier: 8.67.0 - version: 8.67.0(supports-color@7.2.0)(typescript@5.9.3) + specifier: 8.69.0 + version: 8.69.0(supports-color@7.2.0)(typescript@5.9.3) '@typescript-eslint/utils': - specifier: 8.67.0 - version: 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + specifier: 8.69.0 + version: 8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) ajv: specifier: 8.20.0 version: 8.20.0 @@ -1288,28 +1289,28 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@cspell/cspell-bundled-dicts@10.0.0': - resolution: {integrity: sha512-ci410HEkng2582oOjlRHQtlGXwh+rUC/mVcN9dObLHpKhvPgzn2S6vT56pARstxxZpcCUG/oLhn3dCqdJlVzmA==} + '@cspell/cspell-bundled-dicts@10.0.1': + resolution: {integrity: sha512-WvkSDNX4Uyyj/ZgbPO6L38iFNMfK1EqsH1FteRiI2qLz6QZMXRFrIt12OqiWIplzZDDaVpBH9FCJOPJll0fjCQ==} engines: {node: '>=22.18.0'} - '@cspell/cspell-performance-monitor@10.0.0': - resolution: {integrity: sha512-2vMh2pLt2dg/ArYvWjMP4v9HCm0pRhONsEJyc8oHdZyOYvX7trixX894I0M39+VBf3yWtPCEgYRh1UDXNIZRig==} + '@cspell/cspell-performance-monitor@10.0.1': + resolution: {integrity: sha512-9tVcHXwRnbazUv4WSG0h3MqV4+LgmLNgSALAQUflPPW0EMxTf7C4Dmv9cgxJyCEQrdnVKCr58nPPaahhz9LJUg==} engines: {node: '>=22.18.0'} - '@cspell/cspell-pipe@10.0.0': - resolution: {integrity: sha512-qcgHhQvtEX8LSwIVsWrdUgiGim52lN3jT+ghlkdp72v+nBcGKsS2frEKTmbGLug+xcqppkzs6Q6VmsFp1MGtfA==} + '@cspell/cspell-pipe@10.0.1': + resolution: {integrity: sha512-HPeXMD9AZ3V/qPkvQaPcak+C7cJ2z7JTHN8smd6J8L2aThLRky2cHc2OyeaHPSHB7WA47b4z2n5u5nawZhv5VQ==} engines: {node: '>=22.18.0'} - '@cspell/cspell-resolver@10.0.0': - resolution: {integrity: sha512-8H+IUDB7SmrpcRugQ5f55qG81ZShk6nQRk+natLz41TEY98D8/LCmjHEkh/vhDPph9pVJmNUp7JcM2E1UHEa2g==} + '@cspell/cspell-resolver@10.0.1': + resolution: {integrity: sha512-PIzkZHD1fGUQx1XteK2d1iQ0Mzq/maYcoB4jkvAiiR6WqP3MWYNKFdI9z+R5pOq5KgMfW+5Ig1q0oSR6h8irlA==} engines: {node: '>=22.18.0'} - '@cspell/cspell-service-bus@10.0.0': - resolution: {integrity: sha512-V7eigqg/TOoKwNK4Q18wr9KGxA8U5SFcoWVS8RyAxv4mQ+yNKHhvHEbRBifjPbQDer66afOrclb2UbqkIy2SOw==} + '@cspell/cspell-service-bus@10.0.1': + resolution: {integrity: sha512-y6NcIGP2IdXaBL4PVH8vxsr7K27wzz3Ech87UtUtrDSXAiVEOvXgAIknEOUVp59rTlUE8Rn4IRURC6f/hgMyfw==} engines: {node: '>=22.18.0'} - '@cspell/cspell-types@10.0.0': - resolution: {integrity: sha512-IQA++Idqb8fZzkCbHq3+T+9yG9WpeaBxomOrG2KcR/Pj0CgnovzuApYKL2cc35UWLePboKinMeqEPiweFpHVug==} + '@cspell/cspell-types@10.0.1': + resolution: {integrity: sha512-kLgLShnWADDVreKC63pBrWkcvxgZzFIfO34Jhx/SWfuOIA3cD8AXT+HjyuLfoGJ7mUb58hv2kUziKzEy4INb1w==} engines: {node: '>=22.18.0'} '@cspell/dict-ada@4.1.1': @@ -1324,11 +1325,11 @@ packages: '@cspell/dict-bash@4.2.3': resolution: {integrity: sha512-ljUZoKHbDqw5Sx0qpL2qTUlmkmr+vhZH/sCNrNaBZKTbdgiswErSnIF1jRbGmEitJNxHRHWsuZyVgnTGfVO1Yw==} - '@cspell/dict-companies@3.2.11': - resolution: {integrity: sha512-0cmafbcz2pTHXLd59eLR1gvDvN6aWAOM0+cIL4LLF9GX9yB2iKDNrKsvs4tJRqutoaTdwNFBbV0FYv+6iCtebQ==} + '@cspell/dict-companies@3.2.12': + resolution: {integrity: sha512-mjiz/N3zWOCsz5VfwMUydSl7uW0OU9H2PnbCNc3RV44Vj6Q59CSp6EYGSGZQxrXU1gpsuZUrwr6QCjNjFOOg5A==} - '@cspell/dict-cpp@7.0.2': - resolution: {integrity: sha512-dfbeERiVNeqmo/npivdR6rDiBCqZi3QtjH2Z0HFcXwpdj6i97dX1xaKyK2GUsO/p4u1TOv63Dmj5Vm48haDpuA==} + '@cspell/dict-cpp@7.1.0': + resolution: {integrity: sha512-rcjycobioQUd9Jm/Y1n8U+sq6ytpZ0iV8AYIo+vyEFfutC5x7g6hL6Fh3bCdh6IporGHRqfEutGK/4sfopD2ZA==} '@cspell/dict-cryptocurrencies@5.0.5': resolution: {integrity: sha512-R68hYYF/rtlE6T/dsObStzN5QZw+0aQBinAXuWCVqwdS7YZo0X33vGMfChkHaiCo3Z2+bkegqHlqxZF4TD3rUA==} @@ -1342,8 +1343,8 @@ packages: '@cspell/dict-dart@2.3.2': resolution: {integrity: sha512-sUiLW56t9gfZcu8iR/5EUg+KYyRD83Cjl3yjDEA2ApVuJvK1HhX+vn4e4k4YfjpUQMag8XO2AaRhARE09+/rqw==} - '@cspell/dict-data-science@2.0.14': - resolution: {integrity: sha512-jl6Ds4u5u5JT+yY30pWQpAbdCHfy3lCcNkLbpL/AZKoUaLEoXbaYsps9xQtvD7DyaiXxiLZkdH2yHHXtoFtZyg==} + '@cspell/dict-data-science@2.0.16': + resolution: {integrity: sha512-M72mxv5asuAnORurz4iXRJ+Tw9XBq6eu7D2Ne7biP0Z1RciKGNxXWu9JycA/KlVvK1hAlKj/fANlXhuEWpXKFg==} '@cspell/dict-django@4.1.6': resolution: {integrity: sha512-SdbSFDGy9ulETqNz15oWv2+kpWLlk8DJYd573xhIkeRdcXOjskRuxjSZPKfW7O3NxN/KEf3gm3IevVOiNuFS+w==} @@ -1357,14 +1358,14 @@ packages: '@cspell/dict-elixir@4.0.8': resolution: {integrity: sha512-CyfphrbMyl4Ms55Vzuj+mNmd693HjBFr9hvU+B2YbFEZprE5AG+EXLYTMRWrXbpds4AuZcvN3deM2XVB80BN/Q==} - '@cspell/dict-en-common-misspellings@2.1.12': - resolution: {integrity: sha512-14Eu6QGqyksqOd4fYPuRb58lK1Va7FQK9XxFsRKnZU8LhL3N+kj7YKDW+7aIaAN/0WGEqslGP6lGbQzNti8Akw==} + '@cspell/dict-en-common-misspellings@2.2.0': + resolution: {integrity: sha512-5PmCHv+AhY0LVNo3bE1FdRKMmw1esKj83GPwLymnVPYNtlI2Jf6y27EjC0azg4zny5keWmku0GQiMf55Fi+qeA==} - '@cspell/dict-en-gb-mit@3.1.24': - resolution: {integrity: sha512-Oowb/Uzkh7OmDRdCcETzMc9imEb4IpLlHJXoYjX8A8DS2X/54gqSjI915JFB8hKtFjBko5OM0BLQ+6cZhFEMmQ==} + '@cspell/dict-en-gb-mit@3.1.25': + resolution: {integrity: sha512-zGODptk24CMrXi49ieG2SUm94CKxEsVF0dYNF+1ZYH0MSsQDZ/PKDlrrbvtBqSupKdPSj0Z9sjOmMNfHHW9ZSg==} - '@cspell/dict-en_us@4.4.35': - resolution: {integrity: sha512-xWpxBCc/FzzMMo/A+0qwARVaIIhR0Ql8yhhv4rvsvg+GfQF+LG9yzg2GwTM5N2rjvzmM3nKuR9zxFZq2I6fJSg==} + '@cspell/dict-en_us@4.4.36': + resolution: {integrity: sha512-2yOhI/+7d1DbfvMljGW4jw8pLqDEsVmnvUXBOCFXtLU2BWgQkrqOJDCNseYjEiEbTp0OtdrWEWWPFSP1TNugQw==} '@cspell/dict-filetypes@3.0.18': resolution: {integrity: sha512-yU7RKD/x1IWmDLzWeiItMwgV+6bUcU/af23uS0+uGiFUbsY1qWV/D4rxlAAO6Z7no3J2z8aZOkYIOvUrJq0Rcw==} @@ -1387,8 +1388,8 @@ packages: '@cspell/dict-git@3.1.0': resolution: {integrity: sha512-KEt9zGkxqGy2q1nwH4CbyqTSv5nadpn8BAlDnzlRcnL0Xb3LX9xTgSGShKvzb0bw35lHoYyLWN2ZKAqbC4pgGQ==} - '@cspell/dict-golang@6.0.26': - resolution: {integrity: sha512-YKA7Xm5KeOd14v5SQ4ll6afe9VSy3a2DWM7L9uBq4u3lXToRBQ1W5PRa+/Q9udd+DTURyVVnQ+7b9cnOlNxaRg==} + '@cspell/dict-golang@6.0.27': + resolution: {integrity: sha512-rRDb2JL8EefaezHGTEclKXCs4eMOS0q/datk94Lm7KQOhlGlzS9FDVZiR1/guh0o+iJCmejQuf5NR2FQeQSWsg==} '@cspell/dict-google@1.0.9': resolution: {integrity: sha512-biL65POqialY0i4g6crj7pR6JnBkbsPovB2WDYkj3H4TuC/QXv7Pu5pdPxeUJA6TSCHI7T5twsO4VSVyRxD9CA==} @@ -1399,8 +1400,8 @@ packages: '@cspell/dict-html-symbol-entities@4.0.5': resolution: {integrity: sha512-429alTD4cE0FIwpMucvSN35Ld87HCyuM8mF731KU5Rm4Je2SG6hmVx7nkBsLyrmH3sQukTcr1GaiZsiEg8svPA==} - '@cspell/dict-html@4.0.15': - resolution: {integrity: sha512-GJYnYKoD9fmo2OI0aySEGZOjThnx3upSUvV7mmqUu8oG+mGgzqm82P/f7OqsuvTaInZZwZbo+PwJQd/yHcyFIw==} + '@cspell/dict-html@4.0.16': + resolution: {integrity: sha512-9B6/Cpb5YVcYgsEAlKudun1yd4ONllYS/ZibKLYea2apPR+l2vQdARnPrP9kTqa7NUNfRKl7m9Fb6k9rjimnow==} '@cspell/dict-java@5.0.12': resolution: {integrity: sha512-qPSNhTcl7LGJ5Qp6VN71H8zqvRQK04S08T67knMq9hTA8U7G1sTKzLmBaDOFhq17vNX/+rT+rbRYp+B5Nwza1A==} @@ -1408,8 +1409,8 @@ packages: '@cspell/dict-julia@1.1.1': resolution: {integrity: sha512-WylJR9TQ2cgwd5BWEOfdO3zvDB+L7kYFm0I9u0s9jKHWQ6yKmfKeMjU9oXxTBxIufhCXm92SKwwVNAC7gjv+yA==} - '@cspell/dict-k8s@1.0.12': - resolution: {integrity: sha512-2LcllTWgaTfYC7DmkMPOn9GsBWsA4DZdlun4po8s2ysTP7CPEnZc1ZfK6pZ2eI4TsZemlUQQ+NZxMe9/QutQxg==} + '@cspell/dict-k8s@1.0.13': + resolution: {integrity: sha512-ELGkS13k7K/NEfVimBSrxVTfqXvOF/Kvxj4I62YxRm8bvHbfoXgrGaOx28lPiNRz+dmu+yYtvuXbnURKtYbC6g==} '@cspell/dict-kotlin@1.1.1': resolution: {integrity: sha512-J3NzzfgmxRvEeOe3qUXnSJQCd38i/dpF9/t3quuWh6gXM+krsAXP75dY1CzDmS8mrJAlBdVBeAW5eAZTD8g86Q==} @@ -1426,22 +1427,22 @@ packages: '@cspell/dict-makefile@1.0.5': resolution: {integrity: sha512-4vrVt7bGiK8Rx98tfRbYo42Xo2IstJkAF4tLLDMNQLkQ86msDlYSKG1ZCk8Abg+EdNcFAjNhXIiNO+w4KflGAQ==} - '@cspell/dict-markdown@2.0.17': - resolution: {integrity: sha512-H8bAxih6U8NOnSPL7R8My+tqjaB4tmnJTjERuz4zYqmf+cH+5xshX3UVgKlwWFcyjsYfv/zEDuRdMctQv1q6HQ==} + '@cspell/dict-markdown@2.0.18': + resolution: {integrity: sha512-KLRSwVrwKDz4n7bl2XQjzvaBZbGgx5QKkP5Ok1b24xaO3TpXsLhAbAn1mItvFlI2tF6Rkt83iYjQcYppywuf5Q==} peerDependencies: '@cspell/dict-css': ^4.1.2 - '@cspell/dict-html': ^4.0.15 + '@cspell/dict-html': ^4.0.16 '@cspell/dict-html-symbol-entities': ^4.0.5 '@cspell/dict-typescript': ^3.2.3 - '@cspell/dict-monkeyc@1.0.12': - resolution: {integrity: sha512-MN7Vs11TdP5mbdNFQP5x2Ac8zOBm97ARg6zM5Sb53YQt/eMvXOMvrep7+/+8NJXs0jkp70bBzjqU4APcqBFNAw==} + '@cspell/dict-monkeyc@1.1.0': + resolution: {integrity: sha512-mc/hgvSy/emOIYtc8kuVEaLUlnaERunfAubfJ5plUXq6s0A69bcukJzUzSt9C0UTCwS28641ILRPv80m3L9QbA==} '@cspell/dict-node@5.0.9': resolution: {integrity: sha512-hO+ga+uYZ/WA4OtiMEyKt5rDUlUyu3nXMf8KVEeqq2msYvAPdldKBGH7lGONg6R/rPhv53Rb+0Y1SLdoK1+7wQ==} - '@cspell/dict-npm@5.2.41': - resolution: {integrity: sha512-To3xsfRmMBYVXtWVEdUgV35M9a/JZ54dSuoY6m6D3uHKKL3I326Wmy4xifZ3PU8MQaWhyEH7zbIcUEtKwTQMcA==} + '@cspell/dict-npm@5.2.46': + resolution: {integrity: sha512-Z5GG59c17UEk6BPZ3lVD/++GvPBI+O05OTGm4sRA/0I/qszbstuYRw+j2//5f8SWzOFXKaGYI8kygwKX9EQZ9Q==} '@cspell/dict-php@4.1.1': resolution: {integrity: sha512-EXelI+4AftmdIGtA8HL8kr4WlUE11OqCSVlnIgZekmTkEGSZdYnkFdiJ5IANSALtlQ1mghKjz+OFqVs6yowgWA==} @@ -1452,14 +1453,14 @@ packages: '@cspell/dict-public-licenses@2.0.16': resolution: {integrity: sha512-EQRrPvEOmwhwWezV+W7LjXbIBjiy6y/shrET6Qcpnk3XANTzfvWflf9PnJ5kId/oKWvihFy0za0AV1JHd03pSQ==} - '@cspell/dict-python@4.2.27': - resolution: {integrity: sha512-Rj6xQgYS4X6ienjgAZF+njA0GRY4oSPouJWv0vfikCTn6EWlfk0V6Dy1HP3Migj1O+IC2NmespgVq+BZNSp8OA==} + '@cspell/dict-python@4.4.0': + resolution: {integrity: sha512-49Eju/a97V6ExAeMJDM2Tk4jsYr0pm+kQzqQrKLAZdrRd6VrLIgseI481EQSmgoVkPPdsuwK1xj9ZlpDQIF4qg==} '@cspell/dict-r@2.1.1': resolution: {integrity: sha512-71Ka+yKfG4ZHEMEmDxc6+blFkeTTvgKbKAbwiwQAuKl3zpqs1Y0vUtwW2N4b3LgmSPhV3ODVY0y4m5ofqDuKMw==} - '@cspell/dict-ruby@5.1.1': - resolution: {integrity: sha512-LHrp84oEV6q1ZxPPyj4z+FdKyq1XAKYPtmGptrd+uwHbrF/Ns5+fy6gtSi7pS+uc0zk3JdO9w/tPK+8N1/7WUA==} + '@cspell/dict-ruby@5.1.2': + resolution: {integrity: sha512-f6Slf11N91ittf71AWlfVVG9GZPezVRBfcMPCTNTWhUsY/VEspkBRZYDhPXjV4df3jqHy5YJs8nlsFcFVF/bMA==} '@cspell/dict-rust@4.1.2': resolution: {integrity: sha512-O1FHrumYcO+HZti3dHfBPUdnDFkI+nbYK3pxYmiM1sr+G0ebOd6qchmswS0Wsc6ZdEVNiPYJY/gZQR6jfW3uOg==} @@ -1470,8 +1471,8 @@ packages: '@cspell/dict-shell@1.2.0': resolution: {integrity: sha512-PVctvT22lJ49niMiakO8xieY7ELCAzjSqhejWR7bAMb5AZ9F4WDEs+XdGMnoVHWeXq7K5rcepLPmEJb+37zzIw==} - '@cspell/dict-software-terms@5.2.2': - resolution: {integrity: sha512-0CaYd6TAsKtEoA7tNswm1iptEblTzEe3UG8beG2cpSTHk7afWIVMtJLgXDv0f/Li67Lf3Z1Jf3JeXR7GsJ2TRw==} + '@cspell/dict-software-terms@5.4.1': + resolution: {integrity: sha512-tY88gnOWj2ax6h+i7BjU7dIH1f3fvY4WypS0a+TpjBgXC318AdOMmy0zK1SzxGNOjhWJfCeCZNVoLFM/DuOG4g==} '@cspell/dict-sql@2.2.1': resolution: {integrity: sha512-qDHF8MpAYCf4pWU8NKbnVGzkoxMNrFqBHyG/dgrlic5EQiKANCLELYtGlX5auIMDLmTf1inA0eNtv74tyRJ/vg==} @@ -1482,8 +1483,8 @@ packages: '@cspell/dict-swift@2.0.6': resolution: {integrity: sha512-PnpNbrIbex2aqU1kMgwEKvCzgbkHtj3dlFLPMqW1vSniop7YxaDTtvTUO4zA++ugYAEL+UK8vYrBwDPTjjvSnA==} - '@cspell/dict-terraform@1.1.3': - resolution: {integrity: sha512-gr6wxCydwSFyyBKhBA2xkENXtVFToheqYYGFvlMZXWjviynXmh+NK/JTvTCk/VHk3+lzbO9EEQKee6VjrAUSbA==} + '@cspell/dict-terraform@1.1.4': + resolution: {integrity: sha512-Ere42ilvMFvQA4GlcN0OKlruMPR6EsvaB+iTHzj2xc+NJGRK64V7yApUcWrOrSgTiM/vhWXPIsK3OMfiAiNdmA==} '@cspell/dict-typescript@3.2.3': resolution: {integrity: sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==} @@ -1494,30 +1495,30 @@ packages: '@cspell/dict-zig@1.0.0': resolution: {integrity: sha512-XibBIxBlVosU06+M6uHWkFeT0/pW5WajDRYdXG2CgHnq85b0TI/Ks0FuBJykmsgi2CAD3Qtx8UHFEtl/DSFnAQ==} - '@cspell/dynamic-import@10.0.0': - resolution: {integrity: sha512-fMqu/5Ma1Q5ZCR/Par+Q4pvaTKmx5pKZzQmkwld2hNounVdk2OaIPM9MzpNn6I1mLk5J+wTnIZmfcWNAzNP9aQ==} + '@cspell/dynamic-import@10.0.1': + resolution: {integrity: sha512-mP1gdq00aIcH8HxNMqnH11X6BKxLcneDtFgl/ecjIKnaGKwi44m8AndP5Kr4ODaYdl8UUw9O3dJh7KaQXnLHZQ==} engines: {node: '>=22.18.0'} - '@cspell/eslint-plugin@10.0.0': - resolution: {integrity: sha512-NXXismQgYtAg9w399Oa57TYZpm1u9VLX5LZADjgYz0ZwR/9FVwbOP2yiUisQXODCTAP+fnWGJPmYRiHmSYrW9Q==} + '@cspell/eslint-plugin@10.0.1': + resolution: {integrity: sha512-VgRVWIWyM5bz2d7eC3/F4ON1fhMdrFB0R6slXmJewDDbJMDloSsMsWCSktiFET3a0TX/DTjOGGDzBN9tqGDGPw==} engines: {node: '>=22.18.0'} peerDependencies: eslint: ^8 || ^9 || ^10 - '@cspell/filetypes@10.0.0': - resolution: {integrity: sha512-UP57j9yrDtlCHpFxc/eGho1m8DP5olfu9KRWwd5fiqL9nMSE2rUJtPzQyvqmDwO5bVZt3B+fTVdo4gxuiqw25A==} + '@cspell/filetypes@10.0.1': + resolution: {integrity: sha512-Z5S35giU5IW49fBBq6BksUbE8PC4IYPfaKuwl5Nl9jkf/OkAKiBmCowKX45NzRUQInwK/GSqqIUifrNeI6LdLw==} engines: {node: '>=22.18.0'} - '@cspell/rpc@10.0.0': - resolution: {integrity: sha512-QrpOZMwz2pAjvl6Hky2PauYoMpLCASn3osjn7uKUbgFV70sahyj6tmx4rRgRX7vHu2WQLZev+YsuO4EujiBDOg==} + '@cspell/rpc@10.0.1': + resolution: {integrity: sha512-axSRKv3zEAmBm66iD/FV/MPmE4/Yf7c3PZiwTW894Yd3iEhtn3KPKeTrqQ2/tDrhB1Z2qTsap/Hue0MK4o5WXg==} engines: {node: '>=22.18.0'} - '@cspell/strong-weak-map@10.0.0': - resolution: {integrity: sha512-JRsato0s2IjYdsng+AGL6oAqgZVQgih5aWKdmxs21H6EdhMaoFDmRE5kXm/RT5a6OMdtnzQM9DqeToqBChWIOQ==} + '@cspell/strong-weak-map@10.0.1': + resolution: {integrity: sha512-lenN1DVyPi8nJLSMSJJ670ddTjyiruLueuSZO1qLcxBqUhgxDt/mALu9N/1m6WdOVcg6m/5cLiZVg2KOo2UzRw==} engines: {node: '>=22.18.0'} - '@cspell/url@10.0.0': - resolution: {integrity: sha512-q+0pHQ8DbqjemyaOn/mTtBRbCuKDqhnsVbZ6J9zkTsxPgMpccjy0s5oLXwomfrrxMRBH+UcbERwtUmE+SbnoIQ==} + '@cspell/url@10.0.1': + resolution: {integrity: sha512-abYYgI29wJhWIfWTYrYuzRYDcHQUQ1N5ylnhxYn1NJnIQMqUWGLbDmt12JABtZ+R6h6UNatQrS7rhP86etvJyQ==} engines: {node: '>=22.18.0'} '@cspotcode/source-map-support@0.8.1': @@ -4336,11 +4337,11 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.67.0': - resolution: {integrity: sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==} + '@typescript-eslint/eslint-plugin@8.69.0': + resolution: {integrity: sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.67.0 + '@typescript-eslint/parser': ^8.69.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' @@ -4350,8 +4351,8 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - '@typescript-eslint/parser@8.67.0': - resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==} + '@typescript-eslint/parser@8.69.0': + resolution: {integrity: sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4363,14 +4364,14 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.66.0': - resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==} + '@typescript-eslint/project-service@8.68.0': + resolution: {integrity: sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.67.0': - resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==} + '@typescript-eslint/project-service@8.69.0': + resolution: {integrity: sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -4383,12 +4384,12 @@ packages: resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.66.0': - resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} + '@typescript-eslint/scope-manager@8.68.0': + resolution: {integrity: sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.67.0': - resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==} + '@typescript-eslint/scope-manager@8.69.0': + resolution: {integrity: sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/tsconfig-utils@8.65.0': @@ -4397,14 +4398,14 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/tsconfig-utils@8.66.0': - resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==} + '@typescript-eslint/tsconfig-utils@8.68.0': + resolution: {integrity: sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/tsconfig-utils@8.67.0': - resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==} + '@typescript-eslint/tsconfig-utils@8.69.0': + resolution: {integrity: sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -4416,8 +4417,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.67.0': - resolution: {integrity: sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==} + '@typescript-eslint/type-utils@8.69.0': + resolution: {integrity: sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4431,12 +4432,12 @@ packages: resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.66.0': - resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} + '@typescript-eslint/types@8.68.0': + resolution: {integrity: sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.67.0': - resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==} + '@typescript-eslint/types@8.69.0': + resolution: {integrity: sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@5.62.0': @@ -4454,14 +4455,14 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/typescript-estree@8.66.0': - resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==} + '@typescript-eslint/typescript-estree@8.68.0': + resolution: {integrity: sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/typescript-estree@8.67.0': - resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==} + '@typescript-eslint/typescript-estree@8.69.0': + resolution: {integrity: sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -4479,15 +4480,15 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.66.0': - resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==} + '@typescript-eslint/utils@8.68.0': + resolution: {integrity: sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.67.0': - resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==} + '@typescript-eslint/utils@8.69.0': + resolution: {integrity: sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4501,12 +4502,12 @@ packages: resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.66.0': - resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} + '@typescript-eslint/visitor-keys@8.68.0': + resolution: {integrity: sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.67.0': - resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==} + '@typescript-eslint/visitor-keys@8.69.0': + resolution: {integrity: sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.2': @@ -4894,16 +4895,6 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} - baseline-browser-mapping@2.10.40: - resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} - engines: {node: '>=6.0.0'} - hasBin: true - - baseline-browser-mapping@2.10.42: - resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} - engines: {node: '>=6.0.0'} - hasBin: true - baseline-browser-mapping@2.11.14: resolution: {integrity: sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==} engines: {node: '>=6.0.0'} @@ -4958,16 +4949,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - browserslist@4.28.5: - resolution: {integrity: sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.28.8: resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -5019,9 +5000,6 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001800: - resolution: {integrity: sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==} - caniuse-lite@1.0.30001803: resolution: {integrity: sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==} @@ -5050,8 +5028,8 @@ packages: change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} - chart.js@4.4.4: - resolution: {integrity: sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==} + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} engines: {pnpm: '>=8'} check-error@2.1.3: @@ -5183,8 +5161,8 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - comment-json@4.6.2: - resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} + comment-json@5.0.0: + resolution: {integrity: sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==} engines: {node: '>= 6'} comment-parser@1.4.7: @@ -5323,36 +5301,36 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - cspell-config-lib@10.0.0: - resolution: {integrity: sha512-HWK7SRnJ3N/kOThw/uzmXmQYCzBxu58Jkq2hHyte1voDl118BeNFoaNRWMpYdHbBi3kCj8gaZu8wGtm+Zmdhxw==} + cspell-config-lib@10.0.1: + resolution: {integrity: sha512-hMpo/0j6k7pbiqrLDOLJKD2IGP9XwhjKf2miiM6p84Xeo4nyuFZaxxDCQ68R851HSYFrrdltgpoipMbj1h2Tnw==} engines: {node: '>=22.18.0'} - cspell-dictionary@10.0.0: - resolution: {integrity: sha512-KubSoEAJO+77KPSSWjoLCz0+MIWVNq3joGTSyxucAZrBSJD64Y1O4BHHr1aj6XHIZwXhWWNScQlrQR3OcIulng==} + cspell-dictionary@10.0.1: + resolution: {integrity: sha512-3cZ659vgsZWkzGQJR/sNqGDVt/OnvTSieLKI76V++4t1bHJfochb9ZrrwsuMsb1VPGiyqClUP1/O6WrefF/FVg==} engines: {node: '>=22.18.0'} - cspell-glob@10.0.0: - resolution: {integrity: sha512-bXS35fMcA9X7GEkfnWBfoPd/vTnxxfXW+YHt6tWxu5fejfs00qUbjWp1oLC9FxRaXWxIkfsYp2mi1k1jYl4RVw==} + cspell-glob@10.0.1: + resolution: {integrity: sha512-7bII9J3aSSpZDwhx7w+zfQXbMxHZQ3be0ilUp5bHrsjz6o07v/NqOHMGcwKdPn1sw2dxDz9sv057xE5pqXnSdw==} engines: {node: '>=22.18.0'} - cspell-grammar@10.0.0: - resolution: {integrity: sha512-49udtYzkcCYEIDJbFOb4IwiAJebOYZnYvG6o6Ep19Tq0Xwjk7i4vxUprNiFNDCWFbcbJRPE9cpwQUVwp5WFGLw==} + cspell-grammar@10.0.1: + resolution: {integrity: sha512-xC9AFYmaI9wsO//a7S5tdDGKGJVD5UEEsTg+Up2fi7lPfXIryisYmV6tePNL1SEg0idYss4ja8LUZ3Mib09BjQ==} engines: {node: '>=22.18.0'} hasBin: true - cspell-io@10.0.0: - resolution: {integrity: sha512-NQCAUhx9DwKApxPuFl7EK1K1XSaQEAPld45yjjwv93xF8rJkEGkgzOwjbqafwAD20eKYv1a7oj/9EC0S5jETSw==} + cspell-io@10.0.1: + resolution: {integrity: sha512-8C2ka07faxflnaqEBO3pektS21XViE/SEHT7F5ZD1ou7FyMR5u3xawTBJSczClfsxLt/WYeztBYrpmGAjmjksw==} engines: {node: '>=22.18.0'} - cspell-lib@10.0.0: - resolution: {integrity: sha512-PowW6JEjuv/F2aFEirZvBxpzHdchOnpsUJbeIcFcai0++taLTbHQObROBEBf7e0S8DnHpVD5TZkqrTME5e44wg==} + cspell-lib@10.0.1: + resolution: {integrity: sha512-RpsIPiLzc4/YMW8BMRKpyJ81x439qjYWcqgdKeXnMkbKM88J9PexzutfFf/4v97v96KzfNitEzMpbI0uj8OeUg==} engines: {node: '>=22.18.0'} - cspell-trie-lib@10.0.0: - resolution: {integrity: sha512-R8qrMx10E/bm3Lecslwxn9XYo5NzSRK1rtandEX5n9UmEYHoBXjZELkg5+TOnV8VgrVaJSK57XtcGrbKp/4kSg==} + cspell-trie-lib@10.0.1: + resolution: {integrity: sha512-BFvhalSkRQFjKrZ//FKK7fRGrZFpifnxB5AwCkzsIsBZqicsfafcQ1xP21qpb0QqyV/IomjNgviG+tRJs+0rMw==} engines: {node: '>=22.18.0'} peerDependencies: - '@cspell/cspell-types': 10.0.0 + '@cspell/cspell-types': 10.0.1 css-declaration-sorter@7.4.0: resolution: {integrity: sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==} @@ -5665,12 +5643,6 @@ packages: engines: {node: '>=0.12.18'} hasBin: true - electron-to-chromium@1.5.382: - resolution: {integrity: sha512-8ETaWbV6SZOrno+G93Ffd9ENsMtetqdnqj4nlfxFW90Sm5GgnuV28Kf62hqQVD6VUgzm7qFQKsTsAPmeUiU3Ug==} - - electron-to-chromium@1.5.389: - resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} - electron-to-chromium@1.5.406: resolution: {integrity: sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==} @@ -5984,8 +5956,8 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - fast-equals@6.0.0: - resolution: {integrity: sha512-PFhhIGgdM79r5Uztdj9Zb6Tt1zKafqVfdMGwVca1z5z6fbX7DmsySSuJd8HiP6I1j505DCS83cLxo5rmSNeVEA==} + fast-equals@6.0.2: + resolution: {integrity: sha512-sAjhj9ZhOxYCGiNMnZLaucOqf5ZeFnHNoKoAZiD9thhJ0N8RP85qJK759/97C/3L7NzzmGVB5uiX9AUpySZmUQ==} engines: {node: '>=6.0.0'} fast-glob@3.3.3: @@ -6447,8 +6419,8 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - ignore@7.0.6: - resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + ignore@7.0.8: + resolution: {integrity: sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==} engines: {node: '>= 4'} image-size@0.5.5: @@ -7246,12 +7218,13 @@ packages: tailwindcss: optional: true - ng2-charts@4.1.1: - resolution: {integrity: sha512-iHwXDbmX86lfeH8VRcsaW2tJATsuAZo4kvvC/Yk2l35zOHjevja1qBvO6BAibiDazi9r9aS6ZRJOqWPsz1pP2w==} + ng2-charts@9.0.0: + resolution: {integrity: sha512-oG4VLbwDmWtSCaBoiuoi14LZR2DrOgG5WKtGm12eAQHrA32l0dc0XwKurY/dilc9bgKAiJgL/qy2hy/rcb8yog==} peerDependencies: - '@angular/cdk': '>=14.0.0' - '@angular/common': '>=14.0.0' - '@angular/core': '>=14.0.0' + '@angular/cdk': '>=20.0.0' + '@angular/common': '>=20.0.0' + '@angular/core': '>=20.0.0' + '@angular/platform-browser': '>=20.0.0' chart.js: ^3.4.0 || ^4.0.0 rxjs: ^6.5.3 || ^7.4.0 @@ -7284,14 +7257,6 @@ packages: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true - node-releases@2.0.50: - resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} - engines: {node: '>=18'} - - node-releases@2.0.51: - resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} - engines: {node: '>=18'} - node-releases@2.0.53: resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} engines: {node: '>=18'} @@ -8460,6 +8425,10 @@ packages: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} + smol-toml@1.8.0: + resolution: {integrity: sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==} + engines: {node: '>= 18'} + socket.io-adapter@2.5.8: resolution: {integrity: sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==} @@ -8989,17 +8958,11 @@ packages: resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} engines: {node: '>=4'} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - update-browserslist-db@1.3.1: resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} hasBin: true peerDependencies: - browserslist: '>= 4.21.0' + browserslist: 4.28.8 uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -9567,23 +9530,23 @@ snapshots: '@angular-eslint/bundled-angular-compiler@22.1.0': {} - '@angular-eslint/eslint-plugin-template@20.7.0(@angular-eslint/template-parser@22.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3))(@typescript-eslint/types@8.67.0)(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3)': + '@angular-eslint/eslint-plugin-template@20.7.0(@angular-eslint/template-parser@22.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3))(@typescript-eslint/types@8.69.0)(@typescript-eslint/utils@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3)': dependencies: '@angular-eslint/bundled-angular-compiler': 20.7.0 '@angular-eslint/template-parser': 22.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) - '@angular-eslint/utils': 20.7.0(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/utils': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@angular-eslint/utils': 20.7.0(@typescript-eslint/utils@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) aria-query: 5.3.2 axobject-query: 4.1.0 eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 5.9.3 - '@angular-eslint/eslint-plugin@20.7.0(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3)': + '@angular-eslint/eslint-plugin@20.7.0(@typescript-eslint/utils@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3)': dependencies: '@angular-eslint/bundled-angular-compiler': 20.7.0 - '@angular-eslint/utils': 20.7.0(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) - '@typescript-eslint/utils': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@angular-eslint/utils': 20.7.0(@typescript-eslint/utils@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -9595,10 +9558,10 @@ snapshots: eslint-scope: 9.1.2 typescript: 5.9.3 - '@angular-eslint/utils@20.7.0(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3)': + '@angular-eslint/utils@20.7.0(@typescript-eslint/utils@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3)': dependencies: '@angular-eslint/bundled-angular-compiler': 20.7.0 - '@typescript-eslint/utils': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 5.9.3 @@ -9672,7 +9635,7 @@ snapshots: '@inquirer/confirm': 5.1.14(@types/node@26.1.1) '@vitejs/plugin-basic-ssl': 2.1.0(vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.43.1)(yaml@2.9.0)) beasties: 0.3.5 - browserslist: 4.28.5 + browserslist: 4.28.8 esbuild: 0.28.1 https-proxy-agent: 7.0.6(supports-color@7.2.0) istanbul-lib-instrument: 6.0.3(supports-color@7.2.0) @@ -11177,26 +11140,26 @@ snapshots: '@colors/colors@1.5.0': {} - '@cspell/cspell-bundled-dicts@10.0.0': + '@cspell/cspell-bundled-dicts@10.0.1': dependencies: '@cspell/dict-ada': 4.1.1 '@cspell/dict-al': 1.1.1 '@cspell/dict-aws': 4.0.17 '@cspell/dict-bash': 4.2.3 - '@cspell/dict-companies': 3.2.11 - '@cspell/dict-cpp': 7.0.2 + '@cspell/dict-companies': 3.2.12 + '@cspell/dict-cpp': 7.1.0 '@cspell/dict-cryptocurrencies': 5.0.5 '@cspell/dict-csharp': 4.0.8 '@cspell/dict-css': 4.1.2 '@cspell/dict-dart': 2.3.2 - '@cspell/dict-data-science': 2.0.14 + '@cspell/dict-data-science': 2.0.16 '@cspell/dict-django': 4.1.6 '@cspell/dict-docker': 1.1.17 '@cspell/dict-dotnet': 5.0.13 '@cspell/dict-elixir': 4.0.8 - '@cspell/dict-en-common-misspellings': 2.1.12 - '@cspell/dict-en-gb-mit': 3.1.24 - '@cspell/dict-en_us': 4.4.35 + '@cspell/dict-en-common-misspellings': 2.2.0 + '@cspell/dict-en-gb-mit': 3.1.25 + '@cspell/dict-en_us': 4.4.36 '@cspell/dict-filetypes': 3.0.18 '@cspell/dict-flutter': 1.1.1 '@cspell/dict-fonts': 4.0.6 @@ -11204,52 +11167,52 @@ snapshots: '@cspell/dict-fullstack': 3.2.9 '@cspell/dict-gaming-terms': 1.1.2 '@cspell/dict-git': 3.1.0 - '@cspell/dict-golang': 6.0.26 + '@cspell/dict-golang': 6.0.27 '@cspell/dict-google': 1.0.9 '@cspell/dict-haskell': 4.0.6 - '@cspell/dict-html': 4.0.15 + '@cspell/dict-html': 4.0.16 '@cspell/dict-html-symbol-entities': 4.0.5 '@cspell/dict-java': 5.0.12 '@cspell/dict-julia': 1.1.1 - '@cspell/dict-k8s': 1.0.12 + '@cspell/dict-k8s': 1.0.13 '@cspell/dict-kotlin': 1.1.1 '@cspell/dict-latex': 5.1.0 '@cspell/dict-lorem-ipsum': 4.0.5 '@cspell/dict-lua': 4.0.8 '@cspell/dict-makefile': 1.0.5 - '@cspell/dict-markdown': 2.0.17(@cspell/dict-css@4.1.2)(@cspell/dict-html-symbol-entities@4.0.5)(@cspell/dict-html@4.0.15)(@cspell/dict-typescript@3.2.3) - '@cspell/dict-monkeyc': 1.0.12 + '@cspell/dict-markdown': 2.0.18(@cspell/dict-css@4.1.2)(@cspell/dict-html-symbol-entities@4.0.5)(@cspell/dict-html@4.0.16)(@cspell/dict-typescript@3.2.3) + '@cspell/dict-monkeyc': 1.1.0 '@cspell/dict-node': 5.0.9 - '@cspell/dict-npm': 5.2.41 + '@cspell/dict-npm': 5.2.46 '@cspell/dict-php': 4.1.1 '@cspell/dict-powershell': 5.0.15 '@cspell/dict-public-licenses': 2.0.16 - '@cspell/dict-python': 4.2.27 + '@cspell/dict-python': 4.4.0 '@cspell/dict-r': 2.1.1 - '@cspell/dict-ruby': 5.1.1 + '@cspell/dict-ruby': 5.1.2 '@cspell/dict-rust': 4.1.2 '@cspell/dict-scala': 5.0.9 '@cspell/dict-shell': 1.2.0 - '@cspell/dict-software-terms': 5.2.2 + '@cspell/dict-software-terms': 5.4.1 '@cspell/dict-sql': 2.2.1 '@cspell/dict-svelte': 1.0.7 '@cspell/dict-swift': 2.0.6 - '@cspell/dict-terraform': 1.1.3 + '@cspell/dict-terraform': 1.1.4 '@cspell/dict-typescript': 3.2.3 '@cspell/dict-vue': 3.0.5 '@cspell/dict-zig': 1.0.0 - '@cspell/cspell-performance-monitor@10.0.0': {} + '@cspell/cspell-performance-monitor@10.0.1': {} - '@cspell/cspell-pipe@10.0.0': {} + '@cspell/cspell-pipe@10.0.1': {} - '@cspell/cspell-resolver@10.0.0': + '@cspell/cspell-resolver@10.0.1': dependencies: global-directory: 5.0.0 - '@cspell/cspell-service-bus@10.0.0': {} + '@cspell/cspell-service-bus@10.0.1': {} - '@cspell/cspell-types@10.0.0': {} + '@cspell/cspell-types@10.0.1': {} '@cspell/dict-ada@4.1.1': {} @@ -11261,9 +11224,9 @@ snapshots: dependencies: '@cspell/dict-shell': 1.2.0 - '@cspell/dict-companies@3.2.11': {} + '@cspell/dict-companies@3.2.12': {} - '@cspell/dict-cpp@7.0.2': {} + '@cspell/dict-cpp@7.1.0': {} '@cspell/dict-cryptocurrencies@5.0.5': {} @@ -11273,7 +11236,7 @@ snapshots: '@cspell/dict-dart@2.3.2': {} - '@cspell/dict-data-science@2.0.14': {} + '@cspell/dict-data-science@2.0.16': {} '@cspell/dict-django@4.1.6': {} @@ -11283,11 +11246,11 @@ snapshots: '@cspell/dict-elixir@4.0.8': {} - '@cspell/dict-en-common-misspellings@2.1.12': {} + '@cspell/dict-en-common-misspellings@2.2.0': {} - '@cspell/dict-en-gb-mit@3.1.24': {} + '@cspell/dict-en-gb-mit@3.1.25': {} - '@cspell/dict-en_us@4.4.35': {} + '@cspell/dict-en_us@4.4.36': {} '@cspell/dict-filetypes@3.0.18': {} @@ -11303,7 +11266,7 @@ snapshots: '@cspell/dict-git@3.1.0': {} - '@cspell/dict-golang@6.0.26': {} + '@cspell/dict-golang@6.0.27': {} '@cspell/dict-google@1.0.9': {} @@ -11311,13 +11274,13 @@ snapshots: '@cspell/dict-html-symbol-entities@4.0.5': {} - '@cspell/dict-html@4.0.15': {} + '@cspell/dict-html@4.0.16': {} '@cspell/dict-java@5.0.12': {} '@cspell/dict-julia@1.1.1': {} - '@cspell/dict-k8s@1.0.12': {} + '@cspell/dict-k8s@1.0.13': {} '@cspell/dict-kotlin@1.1.1': {} @@ -11329,18 +11292,18 @@ snapshots: '@cspell/dict-makefile@1.0.5': {} - '@cspell/dict-markdown@2.0.17(@cspell/dict-css@4.1.2)(@cspell/dict-html-symbol-entities@4.0.5)(@cspell/dict-html@4.0.15)(@cspell/dict-typescript@3.2.3)': + '@cspell/dict-markdown@2.0.18(@cspell/dict-css@4.1.2)(@cspell/dict-html-symbol-entities@4.0.5)(@cspell/dict-html@4.0.16)(@cspell/dict-typescript@3.2.3)': dependencies: '@cspell/dict-css': 4.1.2 - '@cspell/dict-html': 4.0.15 + '@cspell/dict-html': 4.0.16 '@cspell/dict-html-symbol-entities': 4.0.5 '@cspell/dict-typescript': 3.2.3 - '@cspell/dict-monkeyc@1.0.12': {} + '@cspell/dict-monkeyc@1.1.0': {} '@cspell/dict-node@5.0.9': {} - '@cspell/dict-npm@5.2.41': {} + '@cspell/dict-npm@5.2.46': {} '@cspell/dict-php@4.1.1': {} @@ -11348,13 +11311,13 @@ snapshots: '@cspell/dict-public-licenses@2.0.16': {} - '@cspell/dict-python@4.2.27': + '@cspell/dict-python@4.4.0': dependencies: - '@cspell/dict-data-science': 2.0.14 + '@cspell/dict-data-science': 2.0.16 '@cspell/dict-r@2.1.1': {} - '@cspell/dict-ruby@5.1.1': {} + '@cspell/dict-ruby@5.1.2': {} '@cspell/dict-rust@4.1.2': {} @@ -11362,7 +11325,7 @@ snapshots: '@cspell/dict-shell@1.2.0': {} - '@cspell/dict-software-terms@5.2.2': {} + '@cspell/dict-software-terms@5.4.1': {} '@cspell/dict-sql@2.2.1': {} @@ -11370,7 +11333,7 @@ snapshots: '@cspell/dict-swift@2.0.6': {} - '@cspell/dict-terraform@1.1.3': {} + '@cspell/dict-terraform@1.1.4': {} '@cspell/dict-typescript@3.2.3': {} @@ -11378,26 +11341,26 @@ snapshots: '@cspell/dict-zig@1.0.0': {} - '@cspell/dynamic-import@10.0.0': + '@cspell/dynamic-import@10.0.1': dependencies: - '@cspell/url': 10.0.0 + '@cspell/url': 10.0.1 import-meta-resolve: 4.2.0 - '@cspell/eslint-plugin@10.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))': + '@cspell/eslint-plugin@10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))': dependencies: - '@cspell/cspell-types': 10.0.0 - '@cspell/url': 10.0.0 - cspell-lib: 10.0.0 + '@cspell/cspell-types': 10.0.1 + '@cspell/url': 10.0.1 + cspell-lib: 10.0.1 eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) synckit: 0.11.13 - '@cspell/filetypes@10.0.0': {} + '@cspell/filetypes@10.0.1': {} - '@cspell/rpc@10.0.0': {} + '@cspell/rpc@10.0.1': {} - '@cspell/strong-weak-map@10.0.0': {} + '@cspell/strong-weak-map@10.0.1': {} - '@cspell/url@10.0.0': {} + '@cspell/url@10.0.1': {} '@cspotcode/source-map-support@0.8.1': dependencies: @@ -11494,7 +11457,7 @@ snapshots: '@es-joy/jsdoccomment@0.90.0': dependencies: '@types/estree': 1.0.9 - '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/types': 8.69.0 comment-parser: 1.4.7 esquery: 1.7.0 jsdoc-type-pratt-parser: 7.3.0 @@ -12644,13 +12607,13 @@ snapshots: tslib: 2.8.1 yargs-parser: 21.1.1 - '@nx/eslint-plugin@23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@typescript-eslint/parser@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3)': + '@nx/eslint-plugin@23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@typescript-eslint/parser@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@nx/devkit': 23.1.1(nx@23.1.1) '@nx/js': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0) '@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.3) '@typescript-eslint/type-utils': 8.65.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.68.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) confusing-browser-globals: 1.0.11 globals: 17.6.0 jsonc-eslint-parser: 2.4.2 @@ -12658,7 +12621,7 @@ snapshots: semver: 7.6.3 tslib: 2.8.1 optionalDependencies: - '@typescript-eslint/parser': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) eslint-config-prettier: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) transitivePeerDependencies: - '@babel/traverse' @@ -14030,16 +13993,16 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/type-utils': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/parser': 8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/type-utils': 8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.69.0 eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) - ignore: 7.0.6 + ignore: 7.0.8 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -14054,12 +14017,12 @@ snapshots: - supports-color - typescript - '@typescript-eslint/parser@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/parser@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.69.0 debug: 4.4.3(supports-color@7.2.0) eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 5.9.3 @@ -14068,26 +14031,26 @@ snapshots: '@typescript-eslint/project-service@8.65.0(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3) - '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 debug: 4.4.3(supports-color@7.2.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.66.0(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/project-service@8.68.0(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3) - '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 debug: 4.4.3(supports-color@7.2.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.67.0(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/project-service@8.69.0(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3) - '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 debug: 4.4.3(supports-color@7.2.0) typescript: 5.9.3 transitivePeerDependencies: @@ -14103,25 +14066,25 @@ snapshots: '@typescript-eslint/types': 8.65.0 '@typescript-eslint/visitor-keys': 8.65.0 - '@typescript-eslint/scope-manager@8.66.0': + '@typescript-eslint/scope-manager@8.68.0': dependencies: - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/visitor-keys': 8.68.0 - '@typescript-eslint/scope-manager@8.67.0': + '@typescript-eslint/scope-manager@8.69.0': dependencies: - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/visitor-keys': 8.69.0 '@typescript-eslint/tsconfig-utils@8.65.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.66.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.68.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.67.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.69.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -14137,11 +14100,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) debug: 4.4.3(supports-color@7.2.0) eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) ts-api-utils: 2.5.0(typescript@5.9.3) @@ -14153,9 +14116,9 @@ snapshots: '@typescript-eslint/types@8.65.0': {} - '@typescript-eslint/types@8.66.0': {} + '@typescript-eslint/types@8.68.0': {} - '@typescript-eslint/types@8.67.0': {} + '@typescript-eslint/types@8.69.0': {} '@typescript-eslint/typescript-estree@5.62.0(supports-color@7.2.0)(typescript@5.9.3)': dependencies: @@ -14186,12 +14149,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.66.0(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.68.0(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.66.0(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@5.9.3) - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/project-service': 8.68.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@5.9.3) + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/visitor-keys': 8.68.0 debug: 4.4.3(supports-color@7.2.0) minimatch: 10.2.6 semver: 7.8.5 @@ -14201,12 +14164,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.67.0(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.69.0(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.67.0(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3) - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/project-service': 8.69.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/visitor-keys': 8.69.0 debug: 4.4.3(supports-color@7.2.0) minimatch: 10.2.6 semver: 7.8.5 @@ -14242,23 +14205,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.68.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) - '@typescript-eslint/scope-manager': 8.66.0 - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/typescript-estree': 8.66.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@5.9.3) eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.69.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(supports-color@7.2.0)(typescript@5.9.3) eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 5.9.3 transitivePeerDependencies: @@ -14274,14 +14237,14 @@ snapshots: '@typescript-eslint/types': 8.65.0 eslint-visitor-keys: 5.0.1 - '@typescript-eslint/visitor-keys@8.66.0': + '@typescript-eslint/visitor-keys@8.68.0': dependencies: - '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/types': 8.68.0 eslint-visitor-keys: 5.0.1 - '@typescript-eslint/visitor-keys@8.67.0': + '@typescript-eslint/visitor-keys@8.69.0': dependencies: - '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/types': 8.69.0 eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.2': {} @@ -14722,10 +14685,6 @@ snapshots: base64id@2.0.0: {} - baseline-browser-mapping@2.10.40: {} - - baseline-browser-mapping@2.10.42: {} - baseline-browser-mapping@2.11.14: {} basic-auth@2.0.1: @@ -14812,22 +14771,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.2: - dependencies: - baseline-browser-mapping: 2.10.40 - caniuse-lite: 1.0.30001800 - electron-to-chromium: 1.5.382 - node-releases: 2.0.50 - update-browserslist-db: 1.2.3(browserslist@4.28.2) - - browserslist@4.28.5: - dependencies: - baseline-browser-mapping: 2.10.42 - caniuse-lite: 1.0.30001803 - electron-to-chromium: 1.5.389 - node-releases: 2.0.51 - update-browserslist-db: 1.2.3(browserslist@4.28.5) - browserslist@4.28.8: dependencies: baseline-browser-mapping: 2.11.14 @@ -14887,8 +14830,6 @@ snapshots: lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001800: {} - caniuse-lite@1.0.30001803: {} caniuse-lite@1.0.30001809: {} @@ -14914,7 +14855,7 @@ snapshots: change-case@5.4.4: {} - chart.js@4.4.4: + chart.js@4.5.1: dependencies: '@kurkle/color': 0.3.4 @@ -15030,7 +14971,7 @@ snapshots: commander@8.3.0: {} - comment-json@4.6.2: + comment-json@5.0.0: dependencies: array-timsort: 1.0.3 esprima: 4.0.1 @@ -15173,54 +15114,54 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - cspell-config-lib@10.0.0: + cspell-config-lib@10.0.1: dependencies: - '@cspell/cspell-types': 10.0.0 - comment-json: 4.6.2 - smol-toml: 1.6.1 + '@cspell/cspell-types': 10.0.1 + comment-json: 5.0.0 + smol-toml: 1.8.0 yaml: 2.9.0 - cspell-dictionary@10.0.0: + cspell-dictionary@10.0.1: dependencies: - '@cspell/cspell-performance-monitor': 10.0.0 - '@cspell/cspell-pipe': 10.0.0 - '@cspell/cspell-types': 10.0.0 - cspell-trie-lib: 10.0.0(@cspell/cspell-types@10.0.0) - fast-equals: 6.0.0 + '@cspell/cspell-performance-monitor': 10.0.1 + '@cspell/cspell-pipe': 10.0.1 + '@cspell/cspell-types': 10.0.1 + cspell-trie-lib: 10.0.1(@cspell/cspell-types@10.0.1) + fast-equals: 6.0.2 - cspell-glob@10.0.0: + cspell-glob@10.0.1: dependencies: - '@cspell/url': 10.0.0 + '@cspell/url': 10.0.1 picomatch: 4.0.5 - cspell-grammar@10.0.0: + cspell-grammar@10.0.1: dependencies: - '@cspell/cspell-pipe': 10.0.0 - '@cspell/cspell-types': 10.0.0 + '@cspell/cspell-pipe': 10.0.1 + '@cspell/cspell-types': 10.0.1 - cspell-io@10.0.0: + cspell-io@10.0.1: dependencies: - '@cspell/cspell-service-bus': 10.0.0 - '@cspell/url': 10.0.0 + '@cspell/cspell-service-bus': 10.0.1 + '@cspell/url': 10.0.1 - cspell-lib@10.0.0: + cspell-lib@10.0.1: dependencies: - '@cspell/cspell-bundled-dicts': 10.0.0 - '@cspell/cspell-performance-monitor': 10.0.0 - '@cspell/cspell-pipe': 10.0.0 - '@cspell/cspell-resolver': 10.0.0 - '@cspell/cspell-types': 10.0.0 - '@cspell/dynamic-import': 10.0.0 - '@cspell/filetypes': 10.0.0 - '@cspell/rpc': 10.0.0 - '@cspell/strong-weak-map': 10.0.0 - '@cspell/url': 10.0.0 - cspell-config-lib: 10.0.0 - cspell-dictionary: 10.0.0 - cspell-glob: 10.0.0 - cspell-grammar: 10.0.0 - cspell-io: 10.0.0 - cspell-trie-lib: 10.0.0(@cspell/cspell-types@10.0.0) + '@cspell/cspell-bundled-dicts': 10.0.1 + '@cspell/cspell-performance-monitor': 10.0.1 + '@cspell/cspell-pipe': 10.0.1 + '@cspell/cspell-resolver': 10.0.1 + '@cspell/cspell-types': 10.0.1 + '@cspell/dynamic-import': 10.0.1 + '@cspell/filetypes': 10.0.1 + '@cspell/rpc': 10.0.1 + '@cspell/strong-weak-map': 10.0.1 + '@cspell/url': 10.0.1 + cspell-config-lib: 10.0.1 + cspell-dictionary: 10.0.1 + cspell-glob: 10.0.1 + cspell-grammar: 10.0.1 + cspell-io: 10.0.1 + cspell-trie-lib: 10.0.1(@cspell/cspell-types@10.0.1) env-paths: 4.0.0 gensequence: 8.0.8 import-fresh: 4.0.0 @@ -15229,9 +15170,9 @@ snapshots: vscode-uri: 3.1.0 xdg-basedir: 5.1.0 - cspell-trie-lib@10.0.0(@cspell/cspell-types@10.0.0): + cspell-trie-lib@10.0.1(@cspell/cspell-types@10.0.1): dependencies: - '@cspell/cspell-types': 10.0.0 + '@cspell/cspell-types': 10.0.1 css-declaration-sorter@7.4.0(postcss@8.5.25): dependencies: @@ -15557,10 +15498,6 @@ snapshots: ejs@5.0.1: {} - electron-to-chromium@1.5.382: {} - - electron-to-chromium@1.5.389: {} - electron-to-chromium@1.5.406: {} emoji-regex@10.6.0: {} @@ -15869,7 +15806,7 @@ snapshots: eslint-plugin-storybook@10.4.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(storybook@10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@7.2.0)(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.68.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) storybook: 10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) transitivePeerDependencies: @@ -16071,7 +16008,7 @@ snapshots: fast-diff@1.3.0: {} - fast-equals@6.0.0: {} + fast-equals@6.0.2: {} fast-glob@3.3.3: dependencies: @@ -16604,7 +16541,7 @@ snapshots: ignore@7.0.5: {} - ignore@7.0.6: {} + ignore@7.0.8: {} image-size@0.5.5: optional: true @@ -17365,7 +17302,7 @@ snapshots: '@rollup/wasm-node': 4.62.2 ajv: 8.20.0 ansi-colors: 4.1.3 - browserslist: 4.28.2 + browserslist: 4.28.8 chokidar: 4.0.3 commander: 14.0.3 dependency-graph: 1.0.0 @@ -17386,12 +17323,13 @@ snapshots: optionalDependencies: rollup: 4.62.2 - ng2-charts@4.1.1(@angular/cdk@20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(chart.js@4.4.4)(rxjs@7.8.2): + ng2-charts@9.0.0(@angular/cdk@20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(chart.js@4.5.1)(rxjs@7.8.2): dependencies: '@angular/cdk': 20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/common': 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) - chart.js: 4.4.4 + '@angular/platform-browser': 20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + chart.js: 4.5.1 lodash-es: 4.18.1 rxjs: 7.8.2 tslib: 2.8.1 @@ -17424,10 +17362,6 @@ snapshots: detect-libc: 2.1.2 optional: true - node-releases@2.0.50: {} - - node-releases@2.0.51: {} - node-releases@2.0.53: {} node-schedule@2.1.1: @@ -18862,6 +18796,8 @@ snapshots: smol-toml@1.6.1: {} + smol-toml@1.8.0: {} + socket.io-adapter@2.5.8(supports-color@7.2.0): dependencies: debug: 4.4.3(supports-color@7.2.0) @@ -19459,18 +19395,6 @@ snapshots: upath@2.0.1: optional: true - update-browserslist-db@1.2.3(browserslist@4.28.2): - dependencies: - browserslist: 4.28.2 - escalade: 3.2.0 - picocolors: 1.1.1 - - update-browserslist-db@1.2.3(browserslist@4.28.5): - dependencies: - browserslist: 4.28.5 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.3.1(browserslist@4.28.8): dependencies: browserslist: 4.28.8 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3cf22bafd5..c43ee8e772 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,6 +12,7 @@ overrides: "brace-expansion@5.0.8": "5.0.9" "brace-expansion@<1.1.18": "1.1.18" "socket.io-parser@4.2.6": 4.2.7 + "browserslist@>=4.28.0 <4.28.8": "4.28.8" svgo: 4.0.2 shell-quote: 1.9.0 adm-zip: 0.6.0