diff --git a/.gitattributes b/.gitattributes
index 9befbbf..8f471ed 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -6,4 +6,7 @@
*.cs text
# Declare files that will always have CRLF line endings on checkout.
-*.sln text eol=crlf
\ No newline at end of file
+*.sln text eol=crlf
+
+# GitHub Agentic Workflows lock files
+.github/workflows/*.lock.yml linguist-generated=true merge=ours
diff --git a/.github/workflows/api-sync-check.lock.yml b/.github/workflows/api-sync-check.lock.yml
new file mode 100644
index 0000000..1e33462
--- /dev/null
+++ b/.github/workflows/api-sync-check.lock.yml
@@ -0,0 +1,1515 @@
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"42b773703e2b8d8b28110e2ea7b5ad06a78ad9d95f63e7bb8e85b37a12400f42","body_hash":"02f4e547ca19e7da3189e862da626398d6a8db2074d703e2fb864a1f017e23f1","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"}
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.77.5","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]}
+# ___ _ _
+# / _ \ | | (_)
+# | |_| | __ _ ___ _ __ | |_ _ ___
+# | _ |/ _` |/ _ \ '_ \| __| |/ __|
+# | | | | (_| | __/ | | | |_| | (__
+# \_| |_/\__, |\___|_| |_|\__|_|\___|
+# __/ |
+# _ _ |___/
+# | | | | / _| |
+# | | | | ___ _ __ _ __| |_| | _____ ____
+# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
+# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
+# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
+#
+# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT.
+#
+# To update this file, edit the corresponding .md file 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/
+#
+# Weekly check to keep R3Ext APIs and features in sync with ReactiveUI and DynamicData. Identifies out-of-sync APIs and opens a pull request with the necessary updates.
+#
+# Secrets used:
+# - COPILOT_GITHUB_TOKEN
+# - GH_AW_CI_TRIGGER_TOKEN
+# - GH_AW_GITHUB_MCP_SERVER_TOKEN
+# - GH_AW_GITHUB_TOKEN
+# - GITHUB_TOKEN
+#
+# Custom actions used:
+# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+# - 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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+# - github/gh-aw-actions/setup@v0.77.5
+#
+# Container images used:
+# - ghcr.io/github/gh-aw-firewall/agent:0.25.58
+# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58
+# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58
+# - ghcr.io/github/gh-aw-firewall/squid:0.25.58
+# - ghcr.io/github/gh-aw-mcpg:v0.3.22
+# - ghcr.io/github/github-mcp-server:v1.1.0
+# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14
+
+name: "API Sync Check โ ReactiveUI & DynamicData"
+on:
+ schedule:
+ - cron: "21 13 * * 1"
+ # Friendly format: weekly on Monday (scattered)
+ # skip-if-match: is:pr is:open label:api-sync in:title "[api-sync]" # Skip-if-match processed as search check in pre-activation job
+ workflow_dispatch:
+ inputs:
+ aw_context:
+ default: ""
+ description: "Agent caller context (used internally by Agentic Workflows)."
+ required: false
+ type: string
+
+permissions: {}
+
+concurrency:
+ group: "gh-aw-${{ github.workflow }}"
+
+run-name: "API Sync Check โ ReactiveUI & DynamicData"
+
+jobs:
+ activation:
+ needs: pre_activation
+ if: needs.pre_activation.outputs.activated == 'true'
+ runs-on: ubuntu-slim
+ permissions:
+ actions: read
+ contents: read
+ outputs:
+ comment_id: ""
+ comment_repo: ""
+ engine_id: ${{ steps.generate_aw_info.outputs.engine_id }}
+ lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }}
+ model: ${{ steps.generate_aw_info.outputs.model }}
+ secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
+ 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 }}
+ stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.77.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/api-sync-check.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - 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: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AGENT_VERSION: "1.0.55"
+ GH_AW_INFO_CLI_VERSION: "v0.77.5"
+ GH_AW_INFO_WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_INFO_EXPERIMENTAL: "false"
+ GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
+ GH_AW_INFO_STAGED: "false"
+ GH_AW_INFO_ALLOWED_DOMAINS: '["dotnet"]'
+ GH_AW_INFO_FIREWALL_ENABLED: "true"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_AWMG_VERSION: ""
+ GH_AW_INFO_FIREWALL_TYPE: "squid"
+ GH_AW_INFO_FRONTMATTER_EMOJI: "๐"
+ 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');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs');
+ await main(core, context);
+ - name: Validate COPILOT_GITHUB_TOKEN secret
+ id: validate-secret
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default
+ env:
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ - name: Checkout .github and .agents folders
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ sparse-checkout: |
+ .github
+ .agents
+ .antigravity
+ .claude
+ .codex
+ .crush
+ .gemini
+ .opencode
+ .pi
+ sparse-checkout-cone-mode: true
+ fetch-depth: 1
+ - name: Save agent config folders for base branch restoration
+ env:
+ GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi"
+ GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc"
+ # poutine:ignore untrusted_checkout_exec
+ 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
+ env:
+ GH_AW_WORKFLOW_FILE: "api-sync-check.lock.yml"
+ GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}"
+ with:
+ script: |
+ 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/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.77.5"
+ with:
+ script: |
+ 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/check_version_updates.cjs');
+ await main();
+ - name: Create prompt with built-in context
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl
+ 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) }}
+ GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }}
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ # poutine:ignore untrusted_checkout_exec
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
+ {
+ cat << 'GH_AW_PROMPT_84bb1231bd368268_EOF'
+
+ GH_AW_PROMPT_84bb1231bd368268_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_84bb1231bd368268_EOF'
+
+ Tools: create_pull_request, missing_tool, missing_data, noop
+ GH_AW_PROMPT_84bb1231bd368268_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md"
+ cat << 'GH_AW_PROMPT_84bb1231bd368268_EOF'
+
+ GH_AW_PROMPT_84bb1231bd368268_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
+ cat << 'GH_AW_PROMPT_84bb1231bd368268_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_84bb1231bd368268_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md"
+ cat << 'GH_AW_PROMPT_84bb1231bd368268_EOF'
+
+ {{#runtime-import .github/workflows/api-sync-check.md}}
+ GH_AW_PROMPT_84bb1231bd368268_EOF
+ } > "$GH_AW_PROMPT"
+ - 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_ENGINE_ID: "copilot"
+ with:
+ script: |
+ 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/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_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) }}
+ GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }}
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` โ run `safeoutputs --help` to see available tools'
+ GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }}
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+
+ const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs');
+
+ // Call the substitution function
+ return await substitutePlaceholders({
+ file: process.env.GH_AW_PROMPT,
+ substitutions: {
+ GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A,
+ GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A,
+ GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A,
+ GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE,
+ GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
+ 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_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
+ }
+ });
+ - 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"
+ - 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"
+ - name: Upload activation artifact
+ if: success()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: activation
+ include-hidden-files: true
+ path: |
+ /tmp/gh-aw/aw_info.json
+ /tmp/gh-aw/model_multipliers.json
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/aw-prompts/prompt-template.txt
+ /tmp/gh-aw/aw-prompts/prompt-import-tree.json
+ /tmp/gh-aw/github_rate_limits.jsonl
+ /tmp/gh-aw/base
+ /tmp/gh-aw/.github/agents
+ /tmp/gh-aw/.github/skills
+ if-no-files-found: ignore
+ retention-days: 1
+
+ agent:
+ needs: activation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: read
+ pull-requests: read
+ concurrency:
+ group: "gh-aw-copilot-${{ github.workflow }}"
+ queue: max
+ 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_WORKFLOW_ID_SANITIZED: apisynccheck
+ outputs:
+ agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }}
+ checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
+ effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }}
+ effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }}
+ has_patch: ${{ steps.collect_output.outputs.has_patch }}
+ inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }}
+ mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }}
+ model: ${{ needs.activation.outputs.model }}
+ model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }}
+ output: ${{ steps.collect_output.outputs.output }}
+ output_types: ${{ steps.collect_output.outputs.output_types }}
+ 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 }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.77.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/api-sync-check.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Set runtime paths
+ id: set-runtime-paths
+ run: |
+ {
+ 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: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - name: Create gh-aw temp directory
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh"
+ - name: Configure gh CLI for GitHub Enterprise
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh"
+ env:
+ GH_TOKEN: ${{ github.token }}
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Checkout PR branch
+ id: checkout-pr
+ if: |
+ github.event.pull_request || github.event.issue.pull_request
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ 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');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs');
+ await main();
+ - name: Install GitHub Copilot CLI
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55
+ env:
+ GH_HOST: github.com
+ - name: Install AWF binary
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58
+ - name: Determine automatic lockdown mode for GitHub MCP Server
+ id: determine-automatic-lockdown
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
+ env:
+ GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ 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');
+ await determineAutomaticLockdown(github, context, core);
+ - name: Download activation artifact
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: activation
+ path: /tmp/gh-aw
+ - name: Restore agent config folders from base branch
+ if: steps.checkout-pr.outcome == 'success'
+ env:
+ GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi"
+ GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc"
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh"
+ - name: Restore inline sub-agents from activation artifact
+ env:
+ GH_AW_SUB_AGENT_DIR: ".github/agents"
+ GH_AW_SUB_AGENT_EXT: ".agent.md"
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh"
+ - name: Restore inline skills from activation artifact
+ env:
+ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14
+ - name: Generate Safe Outputs Config
+ 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_5c6db378b7dc3213_EOF'
+ {"create_pull_request":{"allowed_files":["R3Ext/**/*.cs","R3.DynamicData/**/*.cs","R3Ext.Bindings.SourceGenerator/**/*.cs","R3Ext.Bindings.MauiTargets/**/*.cs","docs/**/*.md","README.md"],"draft":true,"labels":["api-sync","automation"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","reviewers":["copilot"],"title_prefix":"[api-sync] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
+ GH_AW_SAFE_OUTPUTS_CONFIG_5c6db378b7dc3213_EOF
+ - name: Generate Safe Outputs Tools
+ env:
+ GH_AW_TOOLS_META_JSON: |
+ {
+ "description_suffixes": {
+ "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[api-sync] \". Labels [\"api-sync\" \"automation\"] will be automatically added. PRs will be created as drafts. Reviewers [\"copilot\"] will be assigned."
+ },
+ "repo_params": {},
+ "dynamic_tools": []
+ }
+ GH_AW_VALIDATION_JSON: |
+ {
+ "create_pull_request": {
+ "defaultMax": 1,
+ "fields": {
+ "base": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ },
+ "body": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "branch": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "draft": {
+ "type": "boolean"
+ },
+ "labels": {
+ "type": "array",
+ "itemType": "string",
+ "itemSanitize": true,
+ "itemMaxLength": 128
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "title": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ }
+ }
+ },
+ "missing_data": {
+ "defaultMax": 20,
+ "fields": {
+ "alternatives": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "context": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "data_type": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ },
+ "reason": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ }
+ }
+ },
+ "missing_tool": {
+ "defaultMax": 20,
+ "fields": {
+ "alternatives": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 512
+ },
+ "reason": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "tool": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ }
+ }
+ },
+ "noop": {
+ "defaultMax": 1,
+ "fields": {
+ "message": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ }
+ }
+ },
+ "report_incomplete": {
+ "defaultMax": 5,
+ "fields": {
+ "details": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "reason": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 1024
+ }
+ }
+ }
+ }
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ 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/generate_safe_outputs_tools.cjs');
+ await main();
+ - name: Generate Safe Outputs MCP Server Config
+ id: safe-outputs-config
+ run: |
+ # Generate a secure random API key (360 bits of entropy, 40+ chars)
+ # Mask immediately to prevent timing vulnerabilities
+ API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
+ echo "::add-mask::${API_KEY}"
+
+ PORT=3001
+
+ # Set outputs for next steps
+ {
+ echo "safe_outputs_api_key=${API_KEY}"
+ echo "safe_outputs_port=${PORT}"
+ } >> "$GITHUB_OUTPUT"
+
+ echo "Safe Outputs MCP server will run on port ${PORT}"
+
+ - name: Start Safe Outputs MCP HTTP Server
+ id: safe-outputs-start
+ env:
+ DEBUG: '*'
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }}
+ GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }}
+ GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json
+ GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json
+ GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs
+ run: |
+ # Environment variables are set above to prevent template injection
+ export DEBUG
+ export GH_AW_SAFE_OUTPUTS
+ export GH_AW_SAFE_OUTPUTS_PORT
+ export GH_AW_SAFE_OUTPUTS_API_KEY
+ export GH_AW_SAFE_OUTPUTS_TOOLS_PATH
+ export GH_AW_SAFE_OUTPUTS_CONFIG_PATH
+ export GH_AW_MCP_LOG_DIR
+
+ bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh"
+
+ - name: Start MCP Gateway
+ id: start-mcp-gateway
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }}
+ GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }}
+ run: |
+ set -eo pipefail
+ mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config"
+
+ # Export gateway environment variables for MCP config and gateway script
+ export MCP_GATEWAY_PORT="8080"
+ export MCP_GATEWAY_DOMAIN="host.docker.internal"
+ 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
+ export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
+ mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
+ export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288"
+ 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')
+ case "${DOCKER_HOST:-}" in
+ unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;;
+ /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;;
+ * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;;
+ esac
+ DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0')
+ export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --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_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 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 GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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 ghcr.io/github/gh-aw-mcpg:v0.3.22'
+
+ mkdir -p /home/runner/.copilot
+ GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
+ cat << GH_AW_MCP_CONFIG_5ff1c5566be662d1_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
+ {
+ "mcpServers": {
+ "safeoutputs": {
+ "type": "http",
+ "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT",
+ "headers": {
+ "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}"
+ },
+ "guard-policies": {
+ "write-sink": {
+ "accept": [
+ "*"
+ ]
+ }
+ }
+ }
+ },
+ "gateway": {
+ "port": $MCP_GATEWAY_PORT,
+ "domain": "${MCP_GATEWAY_DOMAIN}",
+ "apiKey": "${MCP_GATEWAY_API_KEY}",
+ "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
+ }
+ }
+ GH_AW_MCP_CONFIG_5ff1c5566be662d1_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_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');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs');
+ await main();
+ - name: Clean credentials
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh"
+ - name: Audit pre-agent workspace
+ id: pre_agent_audit
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh"
+ - name: Start CLI Proxy
+ env:
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ CLI_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}'
+ CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.22'
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh"
+ - name: Execute GitHub Copilot CLI
+ id: 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
+ 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)
+ printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["*.vsblob.vsassets.io","api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.nuget.org","azuresearch-usnc.nuget.org","azuresearch-ussc.nuget.org","builds.dotnet.microsoft.com","ci.dot.net","dc.services.visualstudio.com","dist.nuget.org","dot.net","dotnet.microsoft.com","dotnetcli.blob.core.windows.net","github.com","host.docker.internal","nuget.org","nuget.pkg.github.com","nugetregistryv2prod.blob.core.windows.net","oneocsp.microsoft.com","pkgs.dev.azure.com","raw.githubusercontent.com","registry.npmjs.org","telemetry.enterprise.githubcopilot.com","www.microsoft.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"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*"],"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*","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-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-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.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*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json"
+ GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs"
+ cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json
+ GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS=""
+ if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then
+ GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw"
+ fi
+ GH_AW_TOOL_CACHE_MOUNT=""
+ GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"
+ 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
+ elif [ -d "/home/runner/work/_tool" ]; then
+ GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro"
+ fi
+ # shellcheck disable=SC1003
+ sudo -E 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_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \
+ -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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_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
+ 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: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }}
+ GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
+ 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_VERSION: v0.77.5
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ GITHUB_AW: true
+ GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_AUTHOR_NAME: github-actions[bot]
+ GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_COMMITTER_NAME: github-actions[bot]
+ RUNNER_TEMP: ${{ runner.temp }}
+ XDG_CONFIG_HOME: /home/runner
+ - name: Stop CLI Proxy
+ if: always()
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_cli_proxy.sh"
+ - name: Detect agent errors
+ if: always()
+ id: detect-agent-errors
+ continue-on-error: true
+ run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs"
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Copy Copilot session state files to logs
+ if: always()
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh"
+ - name: Stop MCP Gateway
+ if: always()
+ 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 }}
+ GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID"
+ - name: Redact secrets in logs
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ 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/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'
+ SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Append agent step summary
+ if: always()
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh"
+ - name: Copy Safe Outputs
+ if: always()
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ run: |
+ mkdir -p /tmp/gh-aw
+ cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true
+ - name: Ingest agent output
+ id: collect_output
+ if: always()
+ 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: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,oneocsp.microsoft.com,pkgs.dev.azure.com,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com,www.microsoft.com"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ with:
+ script: |
+ 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/collect_ndjson_output.cjs');
+ await main();
+ - name: Parse agent logs for step summary
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
+ with:
+ script: |
+ 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_copilot_log.cjs');
+ await main();
+ - name: Parse MCP Gateway logs for step summary
+ if: always()
+ id: parse-mcp-gateway
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ 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_mcp_gateway_log.cjs');
+ await main();
+ - name: Print firewall logs
+ if: always()
+ continue-on-error: true
+ env:
+ AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs
+ run: |
+ # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts
+ # AWF runs with sudo, creating files owned by root
+ sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true
+ # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step)
+ if command -v awf &> /dev/null; then
+ awf logs summary | tee -a "$GITHUB_STEP_SUMMARY"
+ else
+ echo 'AWF binary not installed, skipping firewall log summary'
+ fi
+ - name: Parse token usage for step summary
+ if: always()
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ 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_token_usage.cjs');
+ await main();
+ - name: Print AWF reflect summary
+ if: always()
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ 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/awf_reflect_summary.cjs');
+ await main();
+ - 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
+ - name: Upload agent artifacts
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: agent
+ path: |
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/sandbox/agent/logs/
+ /tmp/gh-aw/redacted-urls.log
+ /tmp/gh-aw/mcp-logs/
+ /tmp/gh-aw/agent_usage.json
+ /tmp/gh-aw/agent-stdio.log
+ /tmp/gh-aw/pre-agent-audit.txt
+ /tmp/gh-aw/agent/
+ /tmp/gh-aw/github_rate_limits.jsonl
+ /tmp/gh-aw/safeoutputs.jsonl
+ /tmp/gh-aw/agent_output.json
+ /tmp/gh-aw/aw-*.patch
+ /tmp/gh-aw/aw-*.bundle
+ /tmp/gh-aw/awf-config.json
+ /tmp/gh-aw/sandbox/firewall/logs/
+ /tmp/gh-aw/sandbox/firewall/audit/
+ /tmp/gh-aw/sandbox/firewall/awf-reflect.json
+ if-no-files-found: ignore
+
+ conclusion:
+ needs:
+ - activation
+ - agent
+ - detection
+ - safe_outputs
+ if: >
+ always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' ||
+ needs.activation.outputs.stale_lock_file_failed == 'true')
+ runs-on: ubuntu-slim
+ permissions:
+ contents: write
+ issues: write
+ pull-requests: write
+ concurrency:
+ group: "gh-aw-conclusion-api-sync-check"
+ cancel-in-progress: false
+ queue: max
+ outputs:
+ incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }}
+ noop_message: ${{ steps.noop.outputs.noop_message }}
+ tools_reported: ${{ steps.missing_tool.outputs.tools_reported }}
+ total_count: ${{ steps.missing_tool.outputs.total_count }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.77.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/api-sync-check.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ id: setup-agent-output-env
+ if: steps.download-agent-output.outcome == 'success'
+ 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"
+ - name: Process no-op messages
+ id: noop
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_NOOP_MAX: "1"
+ GH_AW_WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/api-sync-check.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: "true"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ 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/handle_noop_message.cjs');
+ await main();
+ - name: Log detection run
+ id: detection_runs
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/api-sync-check.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');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs');
+ await main();
+ - name: Record missing tool
+ id: missing_tool
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ 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: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/api-sync-check.md"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ 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/missing_tool.cjs');
+ await main();
+ - name: Record incomplete
+ id: report_incomplete
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ 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: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/api-sync-check.md"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ 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/report_incomplete_handler.cjs');
+ await main();
+ - name: Handle agent failure
+ id: handle_agent_failure
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/api-sync-check.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: "api-sync-check"
+ GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168"
+ GH_AW_ENGINE_ID: "copilot"
+ GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }}
+ GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }}
+ GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }}
+ GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }}
+ GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }}
+ GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }}
+ GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }}
+ GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }}
+ GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com"
+ GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }}
+ GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }}
+ GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }}
+ GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }}
+ GH_AW_GROUP_REPORTS: "false"
+ 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_MAX_EFFECTIVE_TOKENS: "25000000"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ 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/handle_agent_failure.cjs');
+ await main();
+
+ detection:
+ needs:
+ - activation
+ - agent
+ if: >
+ always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true')
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }}
+ detection_reason: ${{ steps.detection_conclusion.outputs.reason }}
+ detection_success: ${{ steps.detection_conclusion.outputs.success }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.77.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/api-sync-check.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ id: setup-agent-output-env
+ if: steps.download-agent-output.outcome == 'success'
+ 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"
+ - name: Checkout repository for patch context
+ if: needs.agent.outputs.has_patch == 'true'
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ # --- Threat Detection ---
+ - name: Clean stale firewall files from agent artifact
+ run: |
+ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58
+ - name: Check if detection needed
+ id: detection_guard
+ if: always()
+ env:
+ OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }}
+ HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
+ run: |
+ if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then
+ echo "run_detection=true" >> "$GITHUB_OUTPUT"
+ echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH"
+ else
+ echo "run_detection=false" >> "$GITHUB_OUTPUT"
+ echo "Detection skipped: no agent outputs or patches to analyze"
+ fi
+ - name: Clear MCP Config for detection
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json"
+ rm -f /home/runner/.copilot/mcp-config.json
+ rm -f "$GITHUB_WORKSPACE/.gemini/settings.json"
+ - name: Prepare threat detection files
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ mkdir -p /tmp/gh-aw/threat-detection/aw-prompts
+ 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
+ [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ done
+ for f in /tmp/gh-aw/aw-*.bundle; do
+ [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ done
+ echo "Prepared threat detection files:"
+ ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ - name: Setup threat detection
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ WORKFLOW_DESCRIPTION: "Weekly check to keep R3Ext APIs and features in sync with ReactiveUI and DynamicData. Identifies out-of-sync APIs and opens a pull request with the necessary updates."
+ HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
+ with:
+ script: |
+ 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/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
+ - name: Setup Node.js
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version: '24'
+ package-manager-cache: false
+ - name: Install GitHub Copilot CLI
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55
+ env:
+ GH_HOST: github.com
+ - name: Install AWF binary
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58
+ - name: Execute GitHub Copilot CLI
+ 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
+ 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)
+ printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/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,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json"
+ GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs"
+ cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json
+ GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS=""
+ if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then
+ GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw"
+ fi
+ GH_AW_TOOL_CACHE_MOUNT=""
+ GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"
+ 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
+ elif [ -d "/home/runner/work/_tool" ]; then
+ GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro"
+ fi
+ # shellcheck disable=SC1003
+ sudo -E 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_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \
+ -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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_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
+ 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: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }}
+ GH_AW_PHASE: detection
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_VERSION: v0.77.5
+ GITHUB_API_URL: ${{ github.api_url }}
+ GITHUB_AW: true
+ GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_AUTHOR_NAME: github-actions[bot]
+ GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_COMMITTER_NAME: github-actions[bot]
+ RUNNER_TEMP: ${{ runner.temp }}
+ XDG_CONFIG_HOME: /home/runner
+ - 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
+ 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);
+ }
+ }
+
+ pre_activation:
+ runs-on: ubuntu-slim
+ outputs:
+ activated: ${{ steps.check_membership.outputs.is_team_member == 'true' && steps.check_skip_if_match.outputs.skip_check_ok == 'true' }}
+ matched_command: ''
+ 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 }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.77.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/api-sync-check.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Check team membership for workflow
+ id: check_membership
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_REQUIRED_ROLES: "admin,maintainer,write"
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ 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/check_membership.cjs');
+ await main();
+ - name: Check skip-if-match query
+ id: check_skip_if_match
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_SKIP_QUERY: "is:pr is:open label:api-sync in:title \"[api-sync]\""
+ GH_AW_WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_SKIP_MAX_MATCHES: "1"
+ with:
+ script: |
+ 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/check_skip_if_match.cjs');
+ await main();
+
+ safe_outputs:
+ needs:
+ - activation
+ - agent
+ - detection
+ if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
+ runs-on: ubuntu-slim
+ permissions:
+ contents: write
+ issues: write
+ pull-requests: write
+ timeout-minutes: 15
+ env:
+ GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/api-sync-check"
+ GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }}
+ GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }}
+ GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }}
+ GH_AW_ENGINE_ID: "copilot"
+ GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }}
+ GH_AW_ENGINE_VERSION: "1.0.55"
+ GH_AW_WORKFLOW_EMOJI: "๐"
+ GH_AW_WORKFLOW_ID: "api-sync-check"
+ GH_AW_WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/api-sync-check.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 }}
+ create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }}
+ create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }}
+ created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }}
+ created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }}
+ process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }}
+ 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@v0.77.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "API Sync Check โ ReactiveUI & DynamicData"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/api-sync-check.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ id: setup-agent-output-env
+ if: steps.download-agent-output.outcome == 'success'
+ 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"
+ - name: Download patch artifact
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Extract base branch from agent output
+ id: extract-base-branch
+ if: steps.download-agent-output.outcome == 'success'
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ 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/extract_base_branch_from_agent_output.cjs');
+ await main();
+ - name: Checkout repository (trusted default branch for comment events)
+ if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment')
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ github.event.repository.default_branch }}
+ token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ persist-credentials: false
+ fetch-depth: 1
+ - name: Checkout repository
+ if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment'
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ steps.extract-base-branch.outputs.base-branch || github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }}
+ token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ persist-credentials: false
+ fetch-depth: 1
+ - name: Configure Git credentials
+ if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request')
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Configure GH_HOST for enterprise compatibility
+ id: ghes-host-config
+ shell: bash
+ run: |
+ # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
+ # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
+ GH_HOST="${GITHUB_SERVER_URL#https://}"
+ GH_HOST="${GH_HOST#http://}"
+ echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
+ - name: Process Safe Outputs
+ id: process_safe_outputs
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ 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: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,oneocsp.microsoft.com,pkgs.dev.azure.com,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com,www.microsoft.com"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"allowed_files\":[\"R3Ext/**/*.cs\",\"R3.DynamicData/**/*.cs\",\"R3Ext.Bindings.SourceGenerator/**/*.cs\",\"R3Ext.Bindings.MauiTargets/**/*.cs\",\"docs/**/*.md\",\"README.md\"],\"draft\":true,\"labels\":[\"api-sync\",\"automation\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"reviewers\":[\"copilot\"],\"title_prefix\":\"[api-sync] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
+ GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }}
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ 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/safe_output_handler_manager.cjs');
+ await main();
+ - name: Upload Safe Outputs Items
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: safe-outputs-items
+ path: |
+ /tmp/gh-aw/safe-output-items.jsonl
+ /tmp/gh-aw/temporary-id-map.json
+ if-no-files-found: ignore
+
diff --git a/.github/workflows/api-sync-check.md b/.github/workflows/api-sync-check.md
new file mode 100644
index 0000000..cd7bf2e
--- /dev/null
+++ b/.github/workflows/api-sync-check.md
@@ -0,0 +1,121 @@
+---
+emoji: ๐
+description: Weekly check to keep R3Ext APIs and features in sync with ReactiveUI and DynamicData. Identifies out-of-sync APIs and opens a pull request with the necessary updates.
+on:
+ schedule: weekly on Monday
+ skip-if-match: 'is:pr is:open label:api-sync in:title "[api-sync]"'
+permissions:
+ contents: read
+ issues: read
+ pull-requests: read
+tools:
+ github:
+ mode: gh-proxy
+ toolsets: [default]
+network:
+ allowed: [dotnet]
+safe-outputs:
+ create-pull-request:
+ title-prefix: "[api-sync] "
+ labels: [api-sync, automation]
+ draft: true
+ reviewers: [copilot]
+ allowed-files:
+ - "R3Ext/**/*.cs"
+ - "R3.DynamicData/**/*.cs"
+ - "R3Ext.Bindings.SourceGenerator/**/*.cs"
+ - "R3Ext.Bindings.MauiTargets/**/*.cs"
+ - "docs/**/*.md"
+ - "README.md"
+---
+
+# API Sync Check โ ReactiveUI & DynamicData
+
+## Goal
+
+Keep R3Ext aligned with the latest ReactiveUI and DynamicData APIs and features. Each week:
+
+1. Inspect recent commits and releases in both upstream repos.
+2. Compare against the current R3Ext implementation.
+3. If gaps exist, produce a pull request with the necessary source updates.
+4. If everything is already in sync, call `noop`.
+
+## Context
+
+- **R3Ext repo (this repo)** โ a reactive MVVM + dynamic-data library built on R3.
+ - `R3Ext/` โ ReactiveUI-compatible APIs: commands, interactions, bindings, MVVM helpers.
+ - `R3.DynamicData/` โ DynamicData port: observable caches, lists, operators, change sets.
+- **ReactiveUI upstream**: `https://github.com/reactiveui/reactiveui`
+- **DynamicData upstream**: `https://github.com/reactivemarbles/DynamicData`
+
+## Steps
+
+### 1 โ Gather upstream changes
+
+Use `gh` to inspect each upstream repository.
+
+For **ReactiveUI** (`reactiveui/reactiveui`):
+- Fetch commits from the last 7 days on the default branch.
+- Fetch the latest release tag and release notes.
+- List recently modified public C# files to spot new or changed public APIs (classes, interfaces, public methods/properties).
+
+For **DynamicData** (`reactivemarbles/DynamicData`):
+- Fetch commits from the last 7 days on the default branch.
+- Fetch the latest release tag and release notes.
+- List recently modified public C# files to spot new or changed public APIs.
+
+### 2 โ Inspect the current R3Ext implementation
+
+Read relevant files in this repo to understand the current state:
+- `R3Ext/Commands/` โ RxCommand implementations.
+- `R3Ext/Interactions/` โ RxInteraction implementations.
+- `R3Ext/Bindings/` โ binding helpers and source-generator hooks.
+- `R3Ext/Extensions/` โ observable extension methods.
+- `R3.DynamicData/` โ DynamicData port.
+
+### 3 โ Identify gaps
+
+Compare the upstream public API surface against what R3Ext implements:
+
+- **Missing APIs**: public types or members in the upstream that have no R3Ext equivalent.
+- **Signature drift**: existing R3Ext methods whose signatures no longer match the upstream (parameter types, return types, overloads).
+- **New features**: functionality added upstream (new operators, new command overloads, new change-set types) not yet present in R3Ext.
+- **Removed/deprecated upstream APIs**: flag any that should also be deprecated in R3Ext.
+
+Ignore internal, private, or test-only members. Ignore platform-specific code that does not apply to R3Ext's .NET / MAUI targets.
+
+### 4 โ Apply updates
+
+For each identified gap:
+- Add or update the corresponding C# source file(s) to bring R3Ext in line with the upstream API.
+- Follow the existing code style: XML doc comments for public members, no reflection, AOT-safe patterns.
+- Prefer additive changes (new overloads, new types) over breaking changes. If a breaking change is required, mark it with `// TODO: breaking change โ ` and note it in the PR body.
+- Do not modify test files or benchmark files.
+
+### 5 โ Produce output
+
+**If updates were made**: create a pull request using the `create-pull-request` safe output.
+
+The PR body must include:
+
+```
+## Summary
+
+Brief description of what changed and why.
+
+## ReactiveUI changes
+- List specific upstream commits or release notes that motivated each change.
+
+## DynamicData changes
+- List specific upstream commits or release notes that motivated each change.
+
+## Breaking changes (if any)
+- Describe any breaking changes or API removals.
+```
+
+**If nothing is out of sync**: call `noop` with a short message such as "R3Ext APIs are already in sync with ReactiveUI and DynamicData โ no changes needed."
+
+## Safe Outputs
+
+- Use `create-pull-request` to open a draft PR when source changes are produced.
+- Use `noop` when R3Ext is already in sync and no code changes are needed.
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 6c5ef2b..4892429 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -58,64 +58,45 @@ jobs:
- name: Restore dependencies (libraries and tests only)
run: |
- # Avoid restoring the MAUI SampleApp on Linux runners (iOS/macCatalyst workloads not available)
- dotnet restore R3Ext/R3Ext.csproj
- dotnet restore R3Ext.Bindings.SourceGenerator/R3Ext.Bindings.SourceGenerator.csproj
- dotnet restore R3Ext.Bindings.MauiTargets/R3Ext.Bindings.MauiTargets.csproj
- dotnet restore R3.DynamicData/R3.DynamicData.csproj
+ # Auto-discover packable library projects; excludes tests, samples, and benchmarks
+ # (those set false or omit the property on executable/test targets).
+ # New shippable libraries are picked up automatically once they set true.
+ PACKABLE=$(find . -name "*.csproj" -not -path "*/obj/*" | xargs grep -l 'true' | sort)
+ for proj in $PACKABLE; do
+ dotnet restore "$proj"
+ done
+ # Test projects are not packable but must be restored to run tests
dotnet restore R3Ext.Tests/R3Ext.Tests.csproj
- dotnet restore R3.DynamicData.Tests/R3.DynamicData.Tests.csproj
+ dotnet restore R3Ext.DynamicData.Tests/R3Ext.DynamicData.Tests.csproj
- name: Build Libraries (exclude samples)
run: |
export DOTNET_USE_SHARED_COMPILATION=false
- # Build source generator first (dependency for main library)
- dotnet build R3Ext.Bindings.SourceGenerator/R3Ext.Bindings.SourceGenerator.csproj --configuration Release --no-restore \
- /p:Version=${{ steps.gitversion.outputs.nuGetVersionV2 }} \
- /p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \
- /p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} \
- /p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
-
- # Build main library (uses source generator)
- dotnet build R3Ext/R3Ext.csproj --configuration Release --no-restore \
- /p:Version=${{ steps.gitversion.outputs.nuGetVersionV2 }} \
- /p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \
- /p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} \
- /p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
-
- # Build MAUI targets
- dotnet build R3Ext.Bindings.MauiTargets/R3Ext.Bindings.MauiTargets.csproj --configuration Release --no-restore \
- /p:Version=${{ steps.gitversion.outputs.nuGetVersionV2 }} \
- /p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \
- /p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} \
- /p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
-
- # Build DynamicData library
- dotnet build R3.DynamicData/R3.DynamicData.csproj --configuration Release --no-restore \
- /p:Version=${{ steps.gitversion.outputs.nuGetVersionV2 }} \
- /p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \
- /p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} \
- /p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
+ # Alphabetical sort places R3Ext.Bindings.SourceGenerator before R3Ext (dot < slash),
+ # so the source generator is always built before the main library that references it.
+ # dotnet build also resolves project references automatically, so ordering is belt-and-suspenders.
+ PACKABLE=$(find . -name "*.csproj" -not -path "*/obj/*" | xargs grep -l 'true' | sort)
+ for proj in $PACKABLE; do
+ dotnet build "$proj" --configuration Release --no-restore \
+ /p:Version=${{ steps.gitversion.outputs.nuGetVersionV2 }} \
+ /p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \
+ /p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} \
+ /p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
+ done
- name: Test
run: |
export DOTNET_USE_SHARED_COMPILATION=false
dotnet test R3Ext.Tests/R3Ext.Tests.csproj --configuration Release --verbosity normal
- dotnet test R3.DynamicData.Tests/R3.DynamicData.Tests.csproj --configuration Release --verbosity normal
+ dotnet test R3Ext.DynamicData.Tests/R3Ext.DynamicData.Tests.csproj --configuration Release --verbosity normal
- name: Pack NuGet packages
run: |
- dotnet pack R3Ext/R3Ext.csproj --configuration Release --no-build --output ./artifacts \
- /p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersionV2 }}
-
- dotnet pack R3Ext.Bindings.SourceGenerator/R3Ext.Bindings.SourceGenerator.csproj --configuration Release --no-build --output ./artifacts \
- /p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersionV2 }}
-
- dotnet pack R3Ext.Bindings.MauiTargets/R3Ext.Bindings.MauiTargets.csproj --configuration Release --no-build --output ./artifacts \
- /p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersionV2 }}
-
- dotnet pack R3.DynamicData/R3.DynamicData.csproj --configuration Release --no-build --output ./artifacts \
- /p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersionV2 }}
+ PACKABLE=$(find . -name "*.csproj" -not -path "*/obj/*" | xargs grep -l 'true' | sort)
+ for proj in $PACKABLE; do
+ dotnet pack "$proj" --configuration Release --no-build --output ./artifacts \
+ /p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersionV2 }}
+ done
- name: Upload artifacts
uses: actions/upload-artifact@v4
diff --git a/.github/workflows/doc-sync.lock.yml b/.github/workflows/doc-sync.lock.yml
new file mode 100644
index 0000000..dcee11d
--- /dev/null
+++ b/.github/workflows/doc-sync.lock.yml
@@ -0,0 +1,1461 @@
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"2ee8a9b114bcd379ff3ae0dfd2fb3e66f20dcc9d223414ff40d97cb1838b1f19","body_hash":"6bf42df01c3552240e785a7e5a0222fe6322e03f2d14872681316157a75d3af4","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"}
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.77.5","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]}
+# ___ _ _
+# / _ \ | | (_)
+# | |_| | __ _ ___ _ __ | |_ _ ___
+# | _ |/ _` |/ _ \ '_ \| __| |/ __|
+# | | | | (_| | __/ | | | |_| | (__
+# \_| |_/\__, |\___|_| |_|\__|_|\___|
+# __/ |
+# _ _ |___/
+# | | | | / _| |
+# | | | | ___ _ __ _ __| |_| | _____ ____
+# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
+# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
+# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
+#
+# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT.
+#
+# To update this file, edit the corresponding .md file 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/
+#
+# Daily documentation sync โ identifies doc files out of sync with recent code changes and opens a pull request with the necessary updates.
+#
+# Secrets used:
+# - COPILOT_GITHUB_TOKEN
+# - GH_AW_CI_TRIGGER_TOKEN
+# - GH_AW_GITHUB_MCP_SERVER_TOKEN
+# - GH_AW_GITHUB_TOKEN
+# - GITHUB_TOKEN
+#
+# Custom actions used:
+# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+# - 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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+# - github/gh-aw-actions/setup@v0.77.5
+#
+# Container images used:
+# - ghcr.io/github/gh-aw-firewall/agent:0.25.58
+# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58
+# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58
+# - ghcr.io/github/gh-aw-firewall/squid:0.25.58
+# - ghcr.io/github/gh-aw-mcpg:v0.3.22
+# - ghcr.io/github/github-mcp-server:v1.1.0
+# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14
+
+name: "Documentation Sync"
+on:
+ schedule:
+ - cron: "44 8 * * *"
+ # Friendly format: daily around 8am UTC (scattered)
+ workflow_dispatch:
+ inputs:
+ aw_context:
+ default: ""
+ description: "Agent caller context (used internally by Agentic Workflows)."
+ required: false
+ type: string
+
+permissions: {}
+
+concurrency:
+ group: "gh-aw-${{ github.workflow }}"
+
+run-name: "Documentation Sync"
+
+jobs:
+ activation:
+ runs-on: ubuntu-slim
+ permissions:
+ actions: read
+ contents: read
+ outputs:
+ comment_id: ""
+ comment_repo: ""
+ engine_id: ${{ steps.generate_aw_info.outputs.engine_id }}
+ lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }}
+ model: ${{ steps.generate_aw_info.outputs.model }}
+ secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
+ 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 }}
+ stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.77.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "Documentation Sync"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/doc-sync.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - 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: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AGENT_VERSION: "1.0.55"
+ GH_AW_INFO_CLI_VERSION: "v0.77.5"
+ GH_AW_INFO_WORKFLOW_NAME: "Documentation Sync"
+ GH_AW_INFO_EXPERIMENTAL: "false"
+ GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
+ GH_AW_INFO_STAGED: "false"
+ GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","github"]'
+ GH_AW_INFO_FIREWALL_ENABLED: "true"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_AWMG_VERSION: ""
+ GH_AW_INFO_FIREWALL_TYPE: "squid"
+ GH_AW_INFO_FRONTMATTER_EMOJI: "๐"
+ 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');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs');
+ await main(core, context);
+ - name: Validate COPILOT_GITHUB_TOKEN secret
+ id: validate-secret
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default
+ env:
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ - name: Checkout .github and .agents folders
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ sparse-checkout: |
+ .github
+ .agents
+ .antigravity
+ .claude
+ .codex
+ .crush
+ .gemini
+ .opencode
+ .pi
+ sparse-checkout-cone-mode: true
+ fetch-depth: 1
+ - name: Save agent config folders for base branch restoration
+ env:
+ GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi"
+ GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc"
+ # poutine:ignore untrusted_checkout_exec
+ 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
+ env:
+ GH_AW_WORKFLOW_FILE: "doc-sync.lock.yml"
+ GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}"
+ with:
+ script: |
+ 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/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.77.5"
+ with:
+ script: |
+ 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/check_version_updates.cjs');
+ await main();
+ - name: Create prompt with built-in context
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl
+ 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) }}
+ GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }}
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ # poutine:ignore untrusted_checkout_exec
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
+ {
+ cat << 'GH_AW_PROMPT_8416bd4b1991a649_EOF'
+
+ GH_AW_PROMPT_8416bd4b1991a649_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_8416bd4b1991a649_EOF'
+
+ Tools: create_pull_request, missing_tool, missing_data, noop
+ GH_AW_PROMPT_8416bd4b1991a649_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md"
+ cat << 'GH_AW_PROMPT_8416bd4b1991a649_EOF'
+
+ GH_AW_PROMPT_8416bd4b1991a649_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
+ cat << 'GH_AW_PROMPT_8416bd4b1991a649_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_8416bd4b1991a649_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md"
+ cat << 'GH_AW_PROMPT_8416bd4b1991a649_EOF'
+
+ {{#runtime-import .github/workflows/doc-sync.md}}
+ GH_AW_PROMPT_8416bd4b1991a649_EOF
+ } > "$GH_AW_PROMPT"
+ - 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_ENGINE_ID: "copilot"
+ with:
+ script: |
+ 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/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_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) }}
+ GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }}
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` โ run `safeoutputs --help` to see available tools'
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+
+ const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs');
+
+ // Call the substitution function
+ return await substitutePlaceholders({
+ file: process.env.GH_AW_PROMPT,
+ substitutions: {
+ GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A,
+ GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A,
+ GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A,
+ GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE,
+ GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
+ 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_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST
+ }
+ });
+ - 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"
+ - 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"
+ - name: Upload activation artifact
+ if: success()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: activation
+ include-hidden-files: true
+ path: |
+ /tmp/gh-aw/aw_info.json
+ /tmp/gh-aw/model_multipliers.json
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/aw-prompts/prompt-template.txt
+ /tmp/gh-aw/aw-prompts/prompt-import-tree.json
+ /tmp/gh-aw/github_rate_limits.jsonl
+ /tmp/gh-aw/base
+ /tmp/gh-aw/.github/agents
+ /tmp/gh-aw/.github/skills
+ if-no-files-found: ignore
+ retention-days: 1
+
+ agent:
+ needs: activation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: read
+ pull-requests: read
+ concurrency:
+ group: "gh-aw-copilot-${{ github.workflow }}"
+ queue: max
+ 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_WORKFLOW_ID_SANITIZED: docsync
+ outputs:
+ agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }}
+ checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
+ effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }}
+ effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }}
+ has_patch: ${{ steps.collect_output.outputs.has_patch }}
+ inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }}
+ mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }}
+ model: ${{ needs.activation.outputs.model }}
+ model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }}
+ output: ${{ steps.collect_output.outputs.output }}
+ output_types: ${{ steps.collect_output.outputs.output_types }}
+ 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 }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.77.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "Documentation Sync"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/doc-sync.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Set runtime paths
+ id: set-runtime-paths
+ run: |
+ {
+ 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: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - name: Create gh-aw temp directory
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh"
+ - name: Configure gh CLI for GitHub Enterprise
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh"
+ env:
+ GH_TOKEN: ${{ github.token }}
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Checkout PR branch
+ id: checkout-pr
+ if: |
+ github.event.pull_request || github.event.issue.pull_request
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ 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');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs');
+ await main();
+ - name: Install GitHub Copilot CLI
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55
+ env:
+ GH_HOST: github.com
+ - name: Install AWF binary
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58
+ - name: Determine automatic lockdown mode for GitHub MCP Server
+ id: determine-automatic-lockdown
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
+ env:
+ GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ 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');
+ await determineAutomaticLockdown(github, context, core);
+ - name: Download activation artifact
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: activation
+ path: /tmp/gh-aw
+ - name: Restore agent config folders from base branch
+ if: steps.checkout-pr.outcome == 'success'
+ env:
+ GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi"
+ GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc"
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh"
+ - name: Restore inline sub-agents from activation artifact
+ env:
+ GH_AW_SUB_AGENT_DIR: ".github/agents"
+ GH_AW_SUB_AGENT_EXT: ".agent.md"
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh"
+ - name: Restore inline skills from activation artifact
+ env:
+ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14
+ - name: Generate Safe Outputs Config
+ 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_a4c60688f3da9733_EOF'
+ {"create_pull_request":{"allowed_files":["*.md","docs/**/*.md"],"branch_prefix":"doc-sync/","draft":true,"if_no_changes":"ignore","labels":["documentation"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[doc-sync] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
+ GH_AW_SAFE_OUTPUTS_CONFIG_a4c60688f3da9733_EOF
+ - name: Generate Safe Outputs Tools
+ env:
+ GH_AW_TOOLS_META_JSON: |
+ {
+ "description_suffixes": {
+ "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Branch name will be prefixed with \"doc-sync/\". Title will be prefixed with \"[doc-sync] \". Labels [\"documentation\"] will be automatically added. PRs will be created as drafts."
+ },
+ "repo_params": {},
+ "dynamic_tools": []
+ }
+ GH_AW_VALIDATION_JSON: |
+ {
+ "create_pull_request": {
+ "defaultMax": 1,
+ "fields": {
+ "base": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ },
+ "body": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "branch": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "draft": {
+ "type": "boolean"
+ },
+ "labels": {
+ "type": "array",
+ "itemType": "string",
+ "itemSanitize": true,
+ "itemMaxLength": 128
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "title": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ }
+ }
+ },
+ "missing_data": {
+ "defaultMax": 20,
+ "fields": {
+ "alternatives": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "context": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "data_type": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ },
+ "reason": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ }
+ }
+ },
+ "missing_tool": {
+ "defaultMax": 20,
+ "fields": {
+ "alternatives": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 512
+ },
+ "reason": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "tool": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ }
+ }
+ },
+ "noop": {
+ "defaultMax": 1,
+ "fields": {
+ "message": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ }
+ }
+ },
+ "report_incomplete": {
+ "defaultMax": 5,
+ "fields": {
+ "details": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "reason": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 1024
+ }
+ }
+ }
+ }
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ 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/generate_safe_outputs_tools.cjs');
+ await main();
+ - name: Generate Safe Outputs MCP Server Config
+ id: safe-outputs-config
+ run: |
+ # Generate a secure random API key (360 bits of entropy, 40+ chars)
+ # Mask immediately to prevent timing vulnerabilities
+ API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
+ echo "::add-mask::${API_KEY}"
+
+ PORT=3001
+
+ # Set outputs for next steps
+ {
+ echo "safe_outputs_api_key=${API_KEY}"
+ echo "safe_outputs_port=${PORT}"
+ } >> "$GITHUB_OUTPUT"
+
+ echo "Safe Outputs MCP server will run on port ${PORT}"
+
+ - name: Start Safe Outputs MCP HTTP Server
+ id: safe-outputs-start
+ env:
+ DEBUG: '*'
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }}
+ GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }}
+ GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json
+ GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json
+ GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs
+ run: |
+ # Environment variables are set above to prevent template injection
+ export DEBUG
+ export GH_AW_SAFE_OUTPUTS
+ export GH_AW_SAFE_OUTPUTS_PORT
+ export GH_AW_SAFE_OUTPUTS_API_KEY
+ export GH_AW_SAFE_OUTPUTS_TOOLS_PATH
+ export GH_AW_SAFE_OUTPUTS_CONFIG_PATH
+ export GH_AW_MCP_LOG_DIR
+
+ bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh"
+
+ - name: Start MCP Gateway
+ id: start-mcp-gateway
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }}
+ GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }}
+ run: |
+ set -eo pipefail
+ mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config"
+
+ # Export gateway environment variables for MCP config and gateway script
+ export MCP_GATEWAY_PORT="8080"
+ export MCP_GATEWAY_DOMAIN="host.docker.internal"
+ 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
+ export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
+ mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
+ export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288"
+ 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')
+ case "${DOCKER_HOST:-}" in
+ unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;;
+ /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;;
+ * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;;
+ esac
+ DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0')
+ export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --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_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 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 GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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 ghcr.io/github/gh-aw-mcpg:v0.3.22'
+
+ mkdir -p /home/runner/.copilot
+ GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
+ cat << GH_AW_MCP_CONFIG_d63d35411c644936_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
+ {
+ "mcpServers": {
+ "safeoutputs": {
+ "type": "http",
+ "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT",
+ "headers": {
+ "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}"
+ },
+ "guard-policies": {
+ "write-sink": {
+ "accept": [
+ "*"
+ ]
+ }
+ }
+ }
+ },
+ "gateway": {
+ "port": $MCP_GATEWAY_PORT,
+ "domain": "${MCP_GATEWAY_DOMAIN}",
+ "apiKey": "${MCP_GATEWAY_API_KEY}",
+ "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
+ }
+ }
+ GH_AW_MCP_CONFIG_d63d35411c644936_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_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');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs');
+ await main();
+ - name: Clean credentials
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh"
+ - name: Audit pre-agent workspace
+ id: pre_agent_audit
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh"
+ - name: Start CLI Proxy
+ env:
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ CLI_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}'
+ CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.22'
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh"
+ - name: Execute GitHub Copilot CLI
+ id: 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
+ 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)
+ printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["*.githubusercontent.com","api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","codeload.github.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","docs.github.com","github-cloud.githubusercontent.com","github-cloud.s3.amazonaws.com","github.blog","github.com","github.githubassets.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","lfs.github.com","objects.githubusercontent.com","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","patch-diff.githubusercontent.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"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*"],"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*","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-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-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.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*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json"
+ GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs"
+ cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json
+ GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS=""
+ if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then
+ GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw"
+ fi
+ GH_AW_TOOL_CACHE_MOUNT=""
+ GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"
+ 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
+ elif [ -d "/home/runner/work/_tool" ]; then
+ GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro"
+ fi
+ # shellcheck disable=SC1003
+ sudo -E 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_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \
+ -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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_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
+ 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: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }}
+ GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
+ 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_VERSION: v0.77.5
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ GITHUB_AW: true
+ GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_AUTHOR_NAME: github-actions[bot]
+ GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_COMMITTER_NAME: github-actions[bot]
+ RUNNER_TEMP: ${{ runner.temp }}
+ XDG_CONFIG_HOME: /home/runner
+ - name: Stop CLI Proxy
+ if: always()
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_cli_proxy.sh"
+ - name: Detect agent errors
+ if: always()
+ id: detect-agent-errors
+ continue-on-error: true
+ run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs"
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Copy Copilot session state files to logs
+ if: always()
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh"
+ - name: Stop MCP Gateway
+ if: always()
+ 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 }}
+ GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID"
+ - name: Redact secrets in logs
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ 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/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'
+ SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Append agent step summary
+ if: always()
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh"
+ - name: Copy Safe Outputs
+ if: always()
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ run: |
+ mkdir -p /tmp/gh-aw
+ cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true
+ - name: Ingest agent output
+ id: collect_output
+ if: always()
+ 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: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,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,patch-diff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ with:
+ script: |
+ 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/collect_ndjson_output.cjs');
+ await main();
+ - name: Parse agent logs for step summary
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
+ with:
+ script: |
+ 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_copilot_log.cjs');
+ await main();
+ - name: Parse MCP Gateway logs for step summary
+ if: always()
+ id: parse-mcp-gateway
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ 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_mcp_gateway_log.cjs');
+ await main();
+ - name: Print firewall logs
+ if: always()
+ continue-on-error: true
+ env:
+ AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs
+ run: |
+ # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts
+ # AWF runs with sudo, creating files owned by root
+ sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true
+ # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step)
+ if command -v awf &> /dev/null; then
+ awf logs summary | tee -a "$GITHUB_STEP_SUMMARY"
+ else
+ echo 'AWF binary not installed, skipping firewall log summary'
+ fi
+ - name: Parse token usage for step summary
+ if: always()
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ 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_token_usage.cjs');
+ await main();
+ - name: Print AWF reflect summary
+ if: always()
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ 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/awf_reflect_summary.cjs');
+ await main();
+ - 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
+ - name: Upload agent artifacts
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: agent
+ path: |
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/sandbox/agent/logs/
+ /tmp/gh-aw/redacted-urls.log
+ /tmp/gh-aw/mcp-logs/
+ /tmp/gh-aw/agent_usage.json
+ /tmp/gh-aw/agent-stdio.log
+ /tmp/gh-aw/pre-agent-audit.txt
+ /tmp/gh-aw/agent/
+ /tmp/gh-aw/github_rate_limits.jsonl
+ /tmp/gh-aw/safeoutputs.jsonl
+ /tmp/gh-aw/agent_output.json
+ /tmp/gh-aw/aw-*.patch
+ /tmp/gh-aw/aw-*.bundle
+ /tmp/gh-aw/awf-config.json
+ /tmp/gh-aw/sandbox/firewall/logs/
+ /tmp/gh-aw/sandbox/firewall/audit/
+ /tmp/gh-aw/sandbox/firewall/awf-reflect.json
+ if-no-files-found: ignore
+
+ conclusion:
+ needs:
+ - activation
+ - agent
+ - detection
+ - safe_outputs
+ if: >
+ always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' ||
+ needs.activation.outputs.stale_lock_file_failed == 'true')
+ runs-on: ubuntu-slim
+ permissions:
+ contents: write
+ issues: write
+ pull-requests: write
+ concurrency:
+ group: "gh-aw-conclusion-doc-sync"
+ cancel-in-progress: false
+ queue: max
+ outputs:
+ incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }}
+ noop_message: ${{ steps.noop.outputs.noop_message }}
+ tools_reported: ${{ steps.missing_tool.outputs.tools_reported }}
+ total_count: ${{ steps.missing_tool.outputs.total_count }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.77.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "Documentation Sync"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/doc-sync.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ id: setup-agent-output-env
+ if: steps.download-agent-output.outcome == 'success'
+ 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"
+ - name: Process no-op messages
+ id: noop
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_NOOP_MAX: "1"
+ GH_AW_WORKFLOW_NAME: "Documentation Sync"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/doc-sync.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: "true"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ 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/handle_noop_message.cjs');
+ await main();
+ - name: Log detection run
+ id: detection_runs
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Documentation Sync"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/doc-sync.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');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs');
+ await main();
+ - name: Record missing tool
+ id: missing_tool
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ 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: "Documentation Sync"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/doc-sync.md"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ 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/missing_tool.cjs');
+ await main();
+ - name: Record incomplete
+ id: report_incomplete
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ 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: "Documentation Sync"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/doc-sync.md"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ 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/report_incomplete_handler.cjs');
+ await main();
+ - name: Handle agent failure
+ id: handle_agent_failure
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Documentation Sync"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/doc-sync.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: "doc-sync"
+ GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168"
+ GH_AW_ENGINE_ID: "copilot"
+ GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }}
+ GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }}
+ GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }}
+ GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }}
+ GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }}
+ GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }}
+ GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }}
+ GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }}
+ GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com"
+ GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }}
+ GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }}
+ GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }}
+ GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }}
+ GH_AW_GROUP_REPORTS: "false"
+ 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_MAX_EFFECTIVE_TOKENS: "25000000"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ 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/handle_agent_failure.cjs');
+ await main();
+
+ detection:
+ needs:
+ - activation
+ - agent
+ if: >
+ always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true')
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }}
+ detection_reason: ${{ steps.detection_conclusion.outputs.reason }}
+ detection_success: ${{ steps.detection_conclusion.outputs.success }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.77.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "Documentation Sync"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/doc-sync.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ id: setup-agent-output-env
+ if: steps.download-agent-output.outcome == 'success'
+ 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"
+ - name: Checkout repository for patch context
+ if: needs.agent.outputs.has_patch == 'true'
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ # --- Threat Detection ---
+ - name: Clean stale firewall files from agent artifact
+ run: |
+ 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.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58
+ - name: Check if detection needed
+ id: detection_guard
+ if: always()
+ env:
+ OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }}
+ HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
+ run: |
+ if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then
+ echo "run_detection=true" >> "$GITHUB_OUTPUT"
+ echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH"
+ else
+ echo "run_detection=false" >> "$GITHUB_OUTPUT"
+ echo "Detection skipped: no agent outputs or patches to analyze"
+ fi
+ - name: Clear MCP Config for detection
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json"
+ rm -f /home/runner/.copilot/mcp-config.json
+ rm -f "$GITHUB_WORKSPACE/.gemini/settings.json"
+ - name: Prepare threat detection files
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ mkdir -p /tmp/gh-aw/threat-detection/aw-prompts
+ 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
+ [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ done
+ for f in /tmp/gh-aw/aw-*.bundle; do
+ [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ done
+ echo "Prepared threat detection files:"
+ ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ - name: Setup threat detection
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ WORKFLOW_NAME: "Documentation Sync"
+ WORKFLOW_DESCRIPTION: "Daily documentation sync โ identifies doc files out of sync with recent code changes and opens a pull request with the necessary updates."
+ HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
+ with:
+ script: |
+ 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/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
+ - name: Setup Node.js
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version: '24'
+ package-manager-cache: false
+ - name: Install GitHub Copilot CLI
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55
+ env:
+ GH_HOST: github.com
+ - name: Install AWF binary
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58
+ - name: Execute GitHub Copilot CLI
+ 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
+ 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)
+ printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/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,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json"
+ GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs"
+ cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json
+ GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS=""
+ if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then
+ GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw"
+ fi
+ GH_AW_TOOL_CACHE_MOUNT=""
+ GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"
+ 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
+ elif [ -d "/home/runner/work/_tool" ]; then
+ GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro"
+ fi
+ # shellcheck disable=SC1003
+ sudo -E 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_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \
+ -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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_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
+ 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: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }}
+ GH_AW_PHASE: detection
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_VERSION: v0.77.5
+ GITHUB_API_URL: ${{ github.api_url }}
+ GITHUB_AW: true
+ GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_AUTHOR_NAME: github-actions[bot]
+ GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_COMMITTER_NAME: github-actions[bot]
+ RUNNER_TEMP: ${{ runner.temp }}
+ XDG_CONFIG_HOME: /home/runner
+ - 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
+ 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);
+ }
+ }
+
+ safe_outputs:
+ needs:
+ - activation
+ - agent
+ - detection
+ if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
+ runs-on: ubuntu-slim
+ permissions:
+ contents: write
+ issues: write
+ pull-requests: write
+ timeout-minutes: 15
+ env:
+ GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/doc-sync"
+ GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }}
+ GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }}
+ GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }}
+ GH_AW_ENGINE_ID: "copilot"
+ GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }}
+ GH_AW_ENGINE_VERSION: "1.0.55"
+ GH_AW_WORKFLOW_EMOJI: "๐"
+ GH_AW_WORKFLOW_ID: "doc-sync"
+ GH_AW_WORKFLOW_NAME: "Documentation Sync"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/doc-sync.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 }}
+ create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }}
+ create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }}
+ created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }}
+ created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }}
+ process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }}
+ 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@v0.77.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "Documentation Sync"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/doc-sync.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ id: setup-agent-output-env
+ if: steps.download-agent-output.outcome == 'success'
+ 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"
+ - name: Download patch artifact
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Extract base branch from agent output
+ id: extract-base-branch
+ if: steps.download-agent-output.outcome == 'success'
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ 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/extract_base_branch_from_agent_output.cjs');
+ await main();
+ - name: Checkout repository (trusted default branch for comment events)
+ if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment')
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ github.event.repository.default_branch }}
+ token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ persist-credentials: false
+ fetch-depth: 1
+ - name: Checkout repository
+ if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment'
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ steps.extract-base-branch.outputs.base-branch || github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }}
+ token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ persist-credentials: false
+ fetch-depth: 1
+ - name: Configure Git credentials
+ if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request')
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Configure GH_HOST for enterprise compatibility
+ id: ghes-host-config
+ shell: bash
+ run: |
+ # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
+ # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
+ GH_HOST="${GITHUB_SERVER_URL#https://}"
+ GH_HOST="${GH_HOST#http://}"
+ echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
+ - name: Process Safe Outputs
+ id: process_safe_outputs
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ 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: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,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,patch-diff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"allowed_files\":[\"*.md\",\"docs/**/*.md\"],\"branch_prefix\":\"doc-sync/\",\"draft\":true,\"if_no_changes\":\"ignore\",\"labels\":[\"documentation\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[doc-sync] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
+ GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }}
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ 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/safe_output_handler_manager.cjs');
+ await main();
+ - name: Upload Safe Outputs Items
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: safe-outputs-items
+ path: |
+ /tmp/gh-aw/safe-output-items.jsonl
+ /tmp/gh-aw/temporary-id-map.json
+ if-no-files-found: ignore
+
diff --git a/.github/workflows/doc-sync.md b/.github/workflows/doc-sync.md
new file mode 100644
index 0000000..449b2ac
--- /dev/null
+++ b/.github/workflows/doc-sync.md
@@ -0,0 +1,97 @@
+---
+emoji: ๐
+description: Daily documentation sync โ identifies doc files out of sync with recent code changes and opens a pull request with the necessary updates.
+on:
+ schedule: daily around 8am UTC
+permissions:
+ contents: read
+ issues: read
+ pull-requests: read
+strict: true
+network:
+ allowed: [defaults, github]
+tools:
+ github:
+ mode: gh-proxy
+ toolsets: [default]
+safe-outputs:
+ create-pull-request:
+ title-prefix: "[doc-sync] "
+ branch-prefix: doc-sync/
+ labels: [documentation]
+ draft: true
+ if-no-changes: ignore
+ allowed-files:
+ - "*.md"
+ - "docs/**/*.md"
+---
+
+# Documentation Sync
+
+## Goal
+
+Keep the repository documentation accurate and up to date by identifying markdown files that are out of sync with recent code changes, then opening a pull request with the necessary corrections.
+
+## Steps
+
+### 1. Discover recent code changes
+
+Use `gh` to list commits from the past 7 days on the default branch:
+
+```
+gh api repos/{owner}/{repo}/commits?since=<7-days-ago>&per_page=100
+```
+
+For each commit, retrieve the list of files changed. Focus only on non-documentation source files (`.cs`, `.csproj`, `.sln`, `.props`, `.targets`, `.json`). Collect a deduplicated list of changed source paths.
+
+### 2. Identify documentation files
+
+The documentation files in this repository are:
+
+- `README.md` โ primary project overview, features, installation, quick start, API reference
+- `DEPLOYMENT.md` โ deployment and release process
+- `AOT_REFLECTION_ISSUES.md` โ known AOT/trimming issues and workarounds
+- `docs/ClosureEliminationPlan.md` โ plan and progress for eliminating closures in R3.DynamicData
+- `docs/ClosureEliminationStatus.md` โ current status of the closure elimination effort
+- `docs/LibraryParity.md` โ parity tracking between R3Ext and upstream libraries
+- `docs/MigrationMatrix.md` โ migration guidance matrix
+- `docs/UpstreamChangesReview.md` โ review of changes in upstream dependencies
+
+Read each documentation file using the `edit` or `bash` tool to understand its current content.
+
+### 3. Correlate code changes to documentation
+
+For each changed source file, determine which documentation files describe, reference, or track its functionality. Use the following heuristics:
+
+- Changes in `R3.DynamicData/` โ correlate with `docs/ClosureEliminationPlan.md`, `docs/ClosureEliminationStatus.md`, `docs/LibraryParity.md`
+- Changes in `R3Ext/`, `R3Ext.Bindings.SourceGenerator/`, `R3Ext.Bindings.MauiTargets/` โ correlate with `README.md`, `docs/LibraryParity.md`
+- Changes to `.csproj`, `Directory.Build.props`, `GitVersion.yml` โ correlate with `DEPLOYMENT.md`, `README.md`
+- New public APIs (classes, methods) โ may need documentation in `README.md`
+- AOT-related changes โ correlate with `AOT_REFLECTION_ISSUES.md`
+
+### 4. Identify out-of-sync sections
+
+For each correlated documentation file, read the relevant source code and compare it against what the documentation describes. Identify specific sections that are inaccurate, incomplete, or missing based on the actual code. Examples:
+
+- A status document references a percentage or count that no longer matches reality
+- A doc lists operators or APIs that have been renamed, added, or removed
+- A migration or parity matrix is missing recently added or removed entries
+- A plan document has completed items still marked as pending
+
+### 5. Apply documentation updates
+
+For each out-of-sync section, use the `edit` tool to apply precise, targeted corrections. Keep changes minimal and focused โ do not rewrite sections that remain accurate. Preserve the existing structure, tone, and formatting of each document.
+
+### 6. Produce the pull request
+
+After applying all updates, emit a `create_pull_request` output describing:
+
+- Which documents were changed and why
+- A concise summary of each correction keyed to the source commits that triggered it
+
+If no documentation is out of sync after review, call `noop` with a brief explanation (e.g. "No documentation updates required for changes in the past 7 days.").
+
+## Safe Outputs
+
+- Use `create-pull-request` to open a draft PR with the documentation updates.
+- Use `noop` when no changes are needed.
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 99bb8d5..0c87826 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -17,7 +17,7 @@
"&&",
"dotnet",
"test",
- "R3.DynamicData.Tests/R3.DynamicData.Tests.csproj",
+ "R3Ext.DynamicData.Tests/R3Ext.DynamicData.Tests.csproj",
"--nologo",
"--blame-hang",
"--blame-hang-timeout",
diff --git a/AOT_REFLECTION_ISSUES.md b/AOT_REFLECTION_ISSUES.md
index e4d7c13..911925c 100644
--- a/AOT_REFLECTION_ISSUES.md
+++ b/AOT_REFLECTION_ISSUES.md
@@ -1,6 +1,6 @@
# AOT and Reflection Issues Matrix
-This document tracks all potential AOT (Ahead-of-Time) compilation, reflection, and expression evaluation issues found in R3Ext and R3.DynamicData projects.
+This document tracks all potential AOT (Ahead-of-Time) compilation, reflection, and expression evaluation issues found in R3Ext and R3Ext.DynamicData projects.
## Status Legend
- โณ **Pending** - Not yet addressed
@@ -14,7 +14,7 @@ This document tracks all potential AOT (Ahead-of-Time) compilation, reflection,
## ๐ Summary: All Critical AOT Issues Resolved
**Status:** โ
**Complete**
-**Test Coverage:** 459/459 tests passing (174 R3Ext + 285 R3.DynamicData)
+**Test Coverage:** 459/459 tests passing (174 R3Ext + 285 R3Ext.DynamicData)
**Branch:** feature/aot-reflection-fixes (5 commits)
All critical Expression.Compile() and reflection-based issues have been successfully eliminated. The codebase is now fully compatible with native AOT compilation.
@@ -29,12 +29,12 @@ All critical Expression.Compile() and reflection-based issues have been successf
**AOT Impact:** Expression.Compile() creates dynamic methods that are incompatible with AOT
#### Locations:
-1. **`R3.DynamicData/Cache/ObservableCacheEx.WhenValueChanged.cs`**
+1. **`R3Ext.DynamicData/Cache/ObservableCacheEx.WhenValueChanged.cs`**
- ~~Line 31: `var getter = propertyAccessor.Compile();`~~
- ~~Line 131: `var getter = propertyAccessor.Compile();`~~
- **Context:** Used in WhenValueChanged operators to create property getters from expressions
-2. **`R3.DynamicData/Binding/BindingOptions.cs`**
+2. **`R3Ext.DynamicData/Binding/BindingOptions.cs`**
- ~~Line 156: `propertyAccessor.Compile()(source)`~~
- **Context:** Used in WhenPropertyChanged to compile property access expression
@@ -89,7 +89,7 @@ public static Observable> WhenPropertyChanged<
**AOT Impact:** Runtime reflection on types/properties fails with trimming
#### Locations:
-1. **`R3.DynamicData/Cache/ObservableCacheEx.WhenValueChanged.cs`**
+1. **`R3Ext.DynamicData/Cache/ObservableCacheEx.WhenValueChanged.cs`**
- ~~Lines 233-260: `GetKeySelector()` method~~
- ~~Uses `typeof(TObject).GetProperty()` and `GetProperties()`~~
- ~~Searches for "Id", "[Key]" attribute, or naming patterns~~
@@ -157,7 +157,7 @@ The refactoring maintains the same functionality while being AOT-compatible:
**AOT Impact:** Minor - GetType() for equality check
#### Locations:
-1. **`R3.DynamicData/Cache/Node.cs`**
+1. **`R3Ext.DynamicData/Cache/Node.cs`**
- ~~Line 156: `if (obj.GetType() != GetType())`~~
- **Context:** Equality comparison in Node
@@ -199,7 +199,7 @@ The refactoring maintains the same functionality while being AOT-compatible:
**AOT Impact:** None - Compile-time expression tree inspection is safe
#### Locations:
-1. **`R3.DynamicData/Cache/ObservableCacheEx.WhenValueChanged.cs`**
+1. **`R3Ext.DynamicData/Cache/ObservableCacheEx.WhenValueChanged.cs`**
- Line 22: `if (expression.Body is MemberExpression memberExpr)`
- Line 27: `memberExpr.Member.Name` - reading property name from expression
- **Context:** ExtractPropertyPath helper for source generator key matching
@@ -255,7 +255,7 @@ var compiled = expression.Compile(); // โ Not AOT-safe
### Current AOT Settings:
- **R3Ext.csproj**: `true`
-- **R3.DynamicData.csproj**: No specific AOT settings
+- **R3Ext.DynamicData.csproj**: No specific AOT settings
### Recommended Additions:
```xml
@@ -333,7 +333,7 @@ var compiled = expression.Compile(); // โ Not AOT-safe
### Test Results
- โ
**R3Ext Tests**: 174/174 passing
-- โ
**R3.DynamicData Tests**: 285/285 passing
+- โ
**R3Ext.DynamicData Tests**: 285/285 passing
- โ
**Total**: 459/459 tests passing (100%)
---
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
index 3d0aa71..fb4032a 100644
--- a/DEPLOYMENT.md
+++ b/DEPLOYMENT.md
@@ -75,9 +75,11 @@ When a tag starting with `v` is pushed:
The following packages are published:
- **R3Ext**: Core library with extensions
+- **R3Ext.DynamicData**: DynamicData port โ reactive collections powered by R3
- **R3Ext.Bindings.SourceGenerator**: Data binding source generator
- **R3Ext.Bindings.MauiTargets**: MAUI-specific build targets
-- **R3Ext.PropertyChanged.SourceGenerator**: PropertyChanged source generator
+
+Any project with `true` in its `.csproj` is automatically picked up by CI without further workflow changes.
## GitHub Secrets
@@ -92,15 +94,17 @@ Required secrets in repository settings:
Test package creation locally:
```bash
-# Restore and build
-dotnet restore
-dotnet build -c Release
+# Restore packable libraries and test projects
+PACKABLE=$(find . -name "*.csproj" -not -path "*/obj/*" | xargs grep -l 'true' | sort)
+for proj in $PACKABLE; do dotnet restore "$proj"; done
+dotnet restore R3Ext.Tests/R3Ext.Tests.csproj
+dotnet restore R3Ext.DynamicData.Tests/R3Ext.DynamicData.Tests.csproj
+
+# Build packable libraries
+for proj in $PACKABLE; do dotnet build "$proj" -c Release --no-restore; done
# Create packages
-dotnet pack R3Ext/R3Ext.csproj -c Release -o ./artifacts
-dotnet pack R3Ext.Bindings.SourceGenerator/R3Ext.Bindings.SourceGenerator.csproj -c Release -o ./artifacts
-dotnet pack R3Ext.Bindings.MauiTargets/R3Ext.Bindings.MauiTargets.csproj -c Release -o ./artifacts
-dotnet pack R3Ext.PropertyChanged.SourceGenerator/R3Ext.PropertyChanged.SourceGenerator.csproj -c Release -o ./artifacts
+for proj in $PACKABLE; do dotnet pack "$proj" -c Release --no-build -o ./artifacts; done
# Inspect packages
dotnet nuget verify ./artifacts/*.nupkg
diff --git a/R3Ext.Benchmarks/Phase2CacheOperatorBenchmarks.cs b/R3Ext.Benchmarks/Phase2CacheOperatorBenchmarks.cs
index 89ca163..7015adc 100644
--- a/R3Ext.Benchmarks/Phase2CacheOperatorBenchmarks.cs
+++ b/R3Ext.Benchmarks/Phase2CacheOperatorBenchmarks.cs
@@ -1,8 +1,8 @@
using BenchmarkDotNet.Attributes;
using R3;
-using R3.DynamicData.Cache;
-using R3.DynamicData.Kernel;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.Kernel;
+using R3Ext.DynamicData.List;
namespace R3Ext.Benchmarks;
diff --git a/R3Ext.Benchmarks/R3Ext.Benchmarks.csproj b/R3Ext.Benchmarks/R3Ext.Benchmarks.csproj
index fd1d608..3350911 100644
--- a/R3Ext.Benchmarks/R3Ext.Benchmarks.csproj
+++ b/R3Ext.Benchmarks/R3Ext.Benchmarks.csproj
@@ -30,7 +30,7 @@
-
+
/// Tests for logical operators using ToCollection() to verify UI binding behavior.
diff --git a/R3.DynamicData.Tests/List/MergeChangeSetsTests.cs b/R3Ext.DynamicData.Tests/List/MergeChangeSetsTests.cs
similarity index 98%
rename from R3.DynamicData.Tests/List/MergeChangeSetsTests.cs
rename to R3Ext.DynamicData.Tests/List/MergeChangeSetsTests.cs
index 9768f35..1b5301b 100644
--- a/R3.DynamicData.Tests/List/MergeChangeSetsTests.cs
+++ b/R3Ext.DynamicData.Tests/List/MergeChangeSetsTests.cs
@@ -1,10 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
using Xunit;
-namespace R3.DynamicData.Tests.List;
+namespace R3Ext.DynamicData.Tests.List;
public class MergeChangeSetsTests
{
diff --git a/R3.DynamicData.Tests/List/MergeManyOperatorTests.cs b/R3Ext.DynamicData.Tests/List/MergeManyOperatorTests.cs
similarity index 97%
rename from R3.DynamicData.Tests/List/MergeManyOperatorTests.cs
rename to R3Ext.DynamicData.Tests/List/MergeManyOperatorTests.cs
index d478e33..2c90a4f 100644
--- a/R3.DynamicData.Tests/List/MergeManyOperatorTests.cs
+++ b/R3Ext.DynamicData.Tests/List/MergeManyOperatorTests.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3.
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Tests.List;
+namespace R3Ext.DynamicData.Tests.List;
public class MergeManyOperatorTests
{
diff --git a/R3.DynamicData.Tests/List/NewOperatorsTests.cs b/R3Ext.DynamicData.Tests/List/NewOperatorsTests.cs
similarity index 99%
rename from R3.DynamicData.Tests/List/NewOperatorsTests.cs
rename to R3Ext.DynamicData.Tests/List/NewOperatorsTests.cs
index 7e6823b..9ffa37a 100644
--- a/R3.DynamicData.Tests/List/NewOperatorsTests.cs
+++ b/R3Ext.DynamicData.Tests/List/NewOperatorsTests.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3 - Tests.
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Tests.List;
+namespace R3Ext.DynamicData.Tests.List;
public sealed class NewOperatorsTests : IDisposable
{
diff --git a/R3.DynamicData.Tests/List/SortOperatorTests.cs b/R3Ext.DynamicData.Tests/List/SortOperatorTests.cs
similarity index 99%
rename from R3.DynamicData.Tests/List/SortOperatorTests.cs
rename to R3Ext.DynamicData.Tests/List/SortOperatorTests.cs
index 2f1418c..49432c9 100644
--- a/R3.DynamicData.Tests/List/SortOperatorTests.cs
+++ b/R3Ext.DynamicData.Tests/List/SortOperatorTests.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3.
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Tests.List;
+namespace R3Ext.DynamicData.Tests.List;
public class SortOperatorTests
{
diff --git a/R3.DynamicData.Tests/List/SubscribeManyTests.cs b/R3Ext.DynamicData.Tests/List/SubscribeManyTests.cs
similarity index 94%
rename from R3.DynamicData.Tests/List/SubscribeManyTests.cs
rename to R3Ext.DynamicData.Tests/List/SubscribeManyTests.cs
index defa2a5..1d8f0cd 100644
--- a/R3.DynamicData.Tests/List/SubscribeManyTests.cs
+++ b/R3Ext.DynamicData.Tests/List/SubscribeManyTests.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3.
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Tests.List;
+namespace R3Ext.DynamicData.Tests.List;
#pragma warning disable SA1516
#pragma warning disable SA1503
diff --git a/R3.DynamicData.Tests/List/TransformAsyncTests.cs b/R3Ext.DynamicData.Tests/List/TransformAsyncTests.cs
similarity index 99%
rename from R3.DynamicData.Tests/List/TransformAsyncTests.cs
rename to R3Ext.DynamicData.Tests/List/TransformAsyncTests.cs
index ed11518..7eb92ea 100644
--- a/R3.DynamicData.Tests/List/TransformAsyncTests.cs
+++ b/R3Ext.DynamicData.Tests/List/TransformAsyncTests.cs
@@ -1,9 +1,9 @@
using R3;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
#pragma warning disable SA1503, SA1513, SA1515, SA1107, SA1502, SA1508, SA1516
-namespace R3.DynamicData.Tests.List;
+namespace R3Ext.DynamicData.Tests.List;
public class TransformAsyncTests
{
diff --git a/R3.DynamicData.Tests/List/TransformManyOperatorTests.cs b/R3Ext.DynamicData.Tests/List/TransformManyOperatorTests.cs
similarity index 97%
rename from R3.DynamicData.Tests/List/TransformManyOperatorTests.cs
rename to R3Ext.DynamicData.Tests/List/TransformManyOperatorTests.cs
index 05275ea..45f6762 100644
--- a/R3.DynamicData.Tests/List/TransformManyOperatorTests.cs
+++ b/R3Ext.DynamicData.Tests/List/TransformManyOperatorTests.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3.
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Tests.List;
+namespace R3Ext.DynamicData.Tests.List;
public class TransformManyOperatorTests
{
diff --git a/R3.DynamicData.Tests/List/TransformOperatorTests.cs b/R3Ext.DynamicData.Tests/List/TransformOperatorTests.cs
similarity index 94%
rename from R3.DynamicData.Tests/List/TransformOperatorTests.cs
rename to R3Ext.DynamicData.Tests/List/TransformOperatorTests.cs
index 8a961ea..eb91bfa 100644
--- a/R3.DynamicData.Tests/List/TransformOperatorTests.cs
+++ b/R3Ext.DynamicData.Tests/List/TransformOperatorTests.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3.
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Tests.List;
+namespace R3Ext.DynamicData.Tests.List;
public class TransformOperatorTests
{
diff --git a/R3.DynamicData.Tests/List/VirtualizeOperatorTests.cs b/R3Ext.DynamicData.Tests/List/VirtualizeOperatorTests.cs
similarity index 99%
rename from R3.DynamicData.Tests/List/VirtualizeOperatorTests.cs
rename to R3Ext.DynamicData.Tests/List/VirtualizeOperatorTests.cs
index 45b7c60..1d1bb93 100644
--- a/R3.DynamicData.Tests/List/VirtualizeOperatorTests.cs
+++ b/R3Ext.DynamicData.Tests/List/VirtualizeOperatorTests.cs
@@ -1,8 +1,8 @@
using R3;
-using R3.DynamicData.Cache;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Tests.List;
+namespace R3Ext.DynamicData.Tests.List;
public class VirtualizeOperatorTests
{
diff --git a/R3.DynamicData.Tests/ListAggregateOperatorTests.cs b/R3Ext.DynamicData.Tests/ListAggregateOperatorTests.cs
similarity index 98%
rename from R3.DynamicData.Tests/ListAggregateOperatorTests.cs
rename to R3Ext.DynamicData.Tests/ListAggregateOperatorTests.cs
index 23c85dd..0a39e99 100644
--- a/R3.DynamicData.Tests/ListAggregateOperatorTests.cs
+++ b/R3Ext.DynamicData.Tests/ListAggregateOperatorTests.cs
@@ -1,10 +1,10 @@
// Port of DynamicData to R3.
using System.ComponentModel;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
using Xunit;
-namespace R3.DynamicData.Tests;
+namespace R3Ext.DynamicData.Tests;
public class ListAggregateOperatorTests
{
diff --git a/R3.DynamicData.Tests/ListChangeSetTests.cs b/R3Ext.DynamicData.Tests/ListChangeSetTests.cs
similarity index 95%
rename from R3.DynamicData.Tests/ListChangeSetTests.cs
rename to R3Ext.DynamicData.Tests/ListChangeSetTests.cs
index 226a66d..9488376 100644
--- a/R3.DynamicData.Tests/ListChangeSetTests.cs
+++ b/R3Ext.DynamicData.Tests/ListChangeSetTests.cs
@@ -1,6 +1,6 @@
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Tests;
+namespace R3Ext.DynamicData.Tests;
public class ListChangeSetTests
{
diff --git a/R3.DynamicData.Tests/R3.DynamicData.Tests.csproj b/R3Ext.DynamicData.Tests/R3Ext.DynamicData.Tests.csproj
similarity index 80%
rename from R3.DynamicData.Tests/R3.DynamicData.Tests.csproj
rename to R3Ext.DynamicData.Tests/R3Ext.DynamicData.Tests.csproj
index 10e106d..4518a8e 100644
--- a/R3.DynamicData.Tests/R3.DynamicData.Tests.csproj
+++ b/R3Ext.DynamicData.Tests/R3Ext.DynamicData.Tests.csproj
@@ -24,9 +24,13 @@
+
+
-
+
diff --git a/R3.DynamicData.Tests/SourceCacheTests.cs b/R3Ext.DynamicData.Tests/SourceCacheTests.cs
similarity index 98%
rename from R3.DynamicData.Tests/SourceCacheTests.cs
rename to R3Ext.DynamicData.Tests/SourceCacheTests.cs
index 65150a0..9869e3a 100644
--- a/R3.DynamicData.Tests/SourceCacheTests.cs
+++ b/R3Ext.DynamicData.Tests/SourceCacheTests.cs
@@ -1,10 +1,10 @@
// Port of DynamicData to R3.
using R3;
-using R3.DynamicData.Cache;
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Tests;
+namespace R3Ext.DynamicData.Tests;
public class SourceCacheTests
{
diff --git a/R3.DynamicData.Tests/SourceListTests.cs b/R3Ext.DynamicData.Tests/SourceListTests.cs
similarity index 99%
rename from R3.DynamicData.Tests/SourceListTests.cs
rename to R3Ext.DynamicData.Tests/SourceListTests.cs
index a7346ed..82475d7 100644
--- a/R3.DynamicData.Tests/SourceListTests.cs
+++ b/R3Ext.DynamicData.Tests/SourceListTests.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3.
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Tests;
+namespace R3Ext.DynamicData.Tests;
public class SourceListTests
{
diff --git a/R3.DynamicData.Tests/TransformOperatorTests.cs b/R3Ext.DynamicData.Tests/TransformOperatorTests.cs
similarity index 98%
rename from R3.DynamicData.Tests/TransformOperatorTests.cs
rename to R3Ext.DynamicData.Tests/TransformOperatorTests.cs
index 52ec475..e5cf2ef 100644
--- a/R3.DynamicData.Tests/TransformOperatorTests.cs
+++ b/R3Ext.DynamicData.Tests/TransformOperatorTests.cs
@@ -1,11 +1,11 @@
// Port of DynamicData to R3.
-using R3.DynamicData.Cache;
-using R3.DynamicData.Kernel;
-using R3.DynamicData.Operators;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.Kernel;
+using R3Ext.DynamicData.Operators;
using Xunit;
-namespace R3.DynamicData.Tests;
+namespace R3Ext.DynamicData.Tests;
public class TransformOperatorTests
{
diff --git a/R3.DynamicData/Binding/BindingOptions.cs b/R3Ext.DynamicData/Binding/BindingOptions.cs
similarity index 98%
rename from R3.DynamicData/Binding/BindingOptions.cs
rename to R3Ext.DynamicData/Binding/BindingOptions.cs
index cba63cf..b056c5b 100644
--- a/R3.DynamicData/Binding/BindingOptions.cs
+++ b/R3Ext.DynamicData/Binding/BindingOptions.cs
@@ -1,15 +1,15 @@
-// Supporting types for Bind operator in R3.DynamicData
+// Supporting types for Bind operator in R3Ext.DynamicData
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq.Expressions;
using R3;
-using R3.DynamicData.Kernel;
-using R3.DynamicData.List;
using R3Ext;
+using R3Ext.DynamicData.Kernel;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Binding;
+namespace R3Ext.DynamicData.Binding;
///
/// Options for binding operations.
diff --git a/R3.DynamicData/Cache/Change.cs b/R3Ext.DynamicData/Cache/Change.cs
similarity index 98%
rename from R3.DynamicData/Cache/Change.cs
rename to R3Ext.DynamicData/Cache/Change.cs
index a513808..fa204c9 100644
--- a/R3.DynamicData/Cache/Change.cs
+++ b/R3Ext.DynamicData/Cache/Change.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3.
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Represents a single change to a cached item.
diff --git a/R3.DynamicData/Cache/ChangeSet.cs b/R3Ext.DynamicData/Cache/ChangeSet.cs
similarity index 98%
rename from R3.DynamicData/Cache/ChangeSet.cs
rename to R3Ext.DynamicData/Cache/ChangeSet.cs
index 4021422..9950611 100644
--- a/R3.DynamicData/Cache/ChangeSet.cs
+++ b/R3Ext.DynamicData/Cache/ChangeSet.cs
@@ -2,7 +2,7 @@
using System.Collections;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// A collection of changes for a cache.
diff --git a/R3.DynamicData/Cache/ICacheUpdater.cs b/R3Ext.DynamicData/Cache/ICacheUpdater.cs
similarity index 98%
rename from R3.DynamicData/Cache/ICacheUpdater.cs
rename to R3Ext.DynamicData/Cache/ICacheUpdater.cs
index 9237849..3d7fc78 100644
--- a/R3.DynamicData/Cache/ICacheUpdater.cs
+++ b/R3Ext.DynamicData/Cache/ICacheUpdater.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// API for updating a cache within a batch operation.
diff --git a/R3.DynamicData/Cache/IChangeSet.cs b/R3Ext.DynamicData/Cache/IChangeSet.cs
similarity index 94%
rename from R3.DynamicData/Cache/IChangeSet.cs
rename to R3Ext.DynamicData/Cache/IChangeSet.cs
index b230f54..aceeda8 100644
--- a/R3.DynamicData/Cache/IChangeSet.cs
+++ b/R3Ext.DynamicData/Cache/IChangeSet.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// A collection of changes for a cache.
diff --git a/R3.DynamicData/Cache/IGroup.cs b/R3Ext.DynamicData/Cache/IGroup.cs
similarity index 96%
rename from R3.DynamicData/Cache/IGroup.cs
rename to R3Ext.DynamicData/Cache/IGroup.cs
index b098221..3a361e2 100644
--- a/R3.DynamicData/Cache/IGroup.cs
+++ b/R3Ext.DynamicData/Cache/IGroup.cs
@@ -2,7 +2,7 @@
// Roland Pheasant licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// An update stream which has been grouped by a common key.
diff --git a/R3.DynamicData/Cache/IObservableCache.cs b/R3Ext.DynamicData/Cache/IObservableCache.cs
similarity index 97%
rename from R3.DynamicData/Cache/IObservableCache.cs
rename to R3Ext.DynamicData/Cache/IObservableCache.cs
index 220ad84..e1ad3f9 100644
--- a/R3.DynamicData/Cache/IObservableCache.cs
+++ b/R3Ext.DynamicData/Cache/IObservableCache.cs
@@ -2,9 +2,9 @@
// Copyright (c) 2011-2023 Roland Pheasant. All rights reserved.
// Roland Pheasant licenses this file to you under the MIT license.
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// A read-only observable cache for querying and observing in-memory data.
diff --git a/R3.DynamicData/Cache/ISourceCache.cs b/R3Ext.DynamicData/Cache/ISourceCache.cs
similarity index 99%
rename from R3.DynamicData/Cache/ISourceCache.cs
rename to R3Ext.DynamicData/Cache/ISourceCache.cs
index a77c38d..0250215 100644
--- a/R3.DynamicData/Cache/ISourceCache.cs
+++ b/R3Ext.DynamicData/Cache/ISourceCache.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// An editable cache that exposes an observable change set for tracking modifications.
diff --git a/R3.DynamicData/Cache/Internal/AsyncDisposeMany.cs b/R3Ext.DynamicData/Cache/Internal/AsyncDisposeMany.cs
similarity index 98%
rename from R3.DynamicData/Cache/Internal/AsyncDisposeMany.cs
rename to R3Ext.DynamicData/Cache/Internal/AsyncDisposeMany.cs
index 81a26ec..acf5ba6 100644
--- a/R3.DynamicData/Cache/Internal/AsyncDisposeMany.cs
+++ b/R3Ext.DynamicData/Cache/Internal/AsyncDisposeMany.cs
@@ -1,5 +1,5 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
internal sealed class AsyncDisposeMany
where TObject : notnull, IAsyncDisposable
diff --git a/R3.DynamicData/Cache/Internal/AutoRefresh.cs b/R3Ext.DynamicData/Cache/Internal/AutoRefresh.cs
similarity index 98%
rename from R3.DynamicData/Cache/Internal/AutoRefresh.cs
rename to R3Ext.DynamicData/Cache/Internal/AutoRefresh.cs
index 41477ff..00ab848 100644
--- a/R3.DynamicData/Cache/Internal/AutoRefresh.cs
+++ b/R3Ext.DynamicData/Cache/Internal/AutoRefresh.cs
@@ -6,7 +6,7 @@
// R3 and disposed with their subscription. SAFE.
using System.ComponentModel;
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
internal sealed class AutoRefresh
where TObject : notnull
diff --git a/R3.DynamicData/Cache/Internal/ChangeKeyOperator.cs b/R3Ext.DynamicData/Cache/Internal/ChangeKeyOperator.cs
similarity index 98%
rename from R3.DynamicData/Cache/Internal/ChangeKeyOperator.cs
rename to R3Ext.DynamicData/Cache/Internal/ChangeKeyOperator.cs
index 7e4c395..941feb4 100644
--- a/R3.DynamicData/Cache/Internal/ChangeKeyOperator.cs
+++ b/R3Ext.DynamicData/Cache/Internal/ChangeKeyOperator.cs
@@ -3,9 +3,9 @@
using System;
using System.Collections.Generic;
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
internal sealed class ChangeKeyOperator
where TObject : notnull
diff --git a/R3.DynamicData/Cache/Internal/DisposeMany.cs b/R3Ext.DynamicData/Cache/Internal/DisposeMany.cs
similarity index 98%
rename from R3.DynamicData/Cache/Internal/DisposeMany.cs
rename to R3Ext.DynamicData/Cache/Internal/DisposeMany.cs
index 8be0910..9440090 100644
--- a/R3.DynamicData/Cache/Internal/DisposeMany.cs
+++ b/R3Ext.DynamicData/Cache/Internal/DisposeMany.cs
@@ -1,5 +1,5 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
internal sealed class DisposeMany
where TObject : notnull
diff --git a/R3.DynamicData/Cache/Internal/EnsureUniqueKeys.cs b/R3Ext.DynamicData/Cache/Internal/EnsureUniqueKeys.cs
similarity index 99%
rename from R3.DynamicData/Cache/Internal/EnsureUniqueKeys.cs
rename to R3Ext.DynamicData/Cache/Internal/EnsureUniqueKeys.cs
index 7070f5d..f7081ee 100644
--- a/R3.DynamicData/Cache/Internal/EnsureUniqueKeys.cs
+++ b/R3Ext.DynamicData/Cache/Internal/EnsureUniqueKeys.cs
@@ -2,9 +2,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
internal sealed class EnsureUniqueKeys
where TObject : notnull
diff --git a/R3.DynamicData/Cache/Internal/ExpireAfter.cs b/R3Ext.DynamicData/Cache/Internal/ExpireAfter.cs
similarity index 99%
rename from R3.DynamicData/Cache/Internal/ExpireAfter.cs
rename to R3Ext.DynamicData/Cache/Internal/ExpireAfter.cs
index 2d2767d..45a5d12 100644
--- a/R3.DynamicData/Cache/Internal/ExpireAfter.cs
+++ b/R3Ext.DynamicData/Cache/Internal/ExpireAfter.cs
@@ -3,7 +3,7 @@
// each added to the `disp` CompositeDisposable via .AddTo(disp). The Disposable.Create
// cleanup explicitly iterates and disposes all timers and calls disp.Dispose(), so no
// scheduler-held reference can outlive the operator subscription. SAFE.
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
internal sealed class ExpireAfter
where TObject : notnull
diff --git a/R3.DynamicData/Cache/Internal/FilterOnObservable.cs b/R3Ext.DynamicData/Cache/Internal/FilterOnObservable.cs
similarity index 99%
rename from R3.DynamicData/Cache/Internal/FilterOnObservable.cs
rename to R3Ext.DynamicData/Cache/Internal/FilterOnObservable.cs
index 58e1692..d19eceb 100644
--- a/R3.DynamicData/Cache/Internal/FilterOnObservable.cs
+++ b/R3Ext.DynamicData/Cache/Internal/FilterOnObservable.cs
@@ -1,5 +1,5 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
internal sealed class FilterOnObservable
where TKey : notnull
diff --git a/R3.DynamicData/Cache/Internal/IncludeUpdateWhen.cs b/R3Ext.DynamicData/Cache/Internal/IncludeUpdateWhen.cs
similarity index 98%
rename from R3.DynamicData/Cache/Internal/IncludeUpdateWhen.cs
rename to R3Ext.DynamicData/Cache/Internal/IncludeUpdateWhen.cs
index b621a93..db0b26a 100644
--- a/R3.DynamicData/Cache/Internal/IncludeUpdateWhen.cs
+++ b/R3Ext.DynamicData/Cache/Internal/IncludeUpdateWhen.cs
@@ -1,6 +1,6 @@
// Port of DynamicData IncludeUpdateWhen to R3.
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
internal sealed class IncludeUpdateWhen
where TObject : notnull
diff --git a/R3.DynamicData/Cache/Internal/ManagedGroup.cs b/R3Ext.DynamicData/Cache/Internal/ManagedGroup.cs
similarity index 98%
rename from R3.DynamicData/Cache/Internal/ManagedGroup.cs
rename to R3Ext.DynamicData/Cache/Internal/ManagedGroup.cs
index cdc69ac..d793ef7 100644
--- a/R3.DynamicData/Cache/Internal/ManagedGroup.cs
+++ b/R3Ext.DynamicData/Cache/Internal/ManagedGroup.cs
@@ -5,7 +5,7 @@
using System;
using System.Collections.Generic;
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
internal sealed class ManagedGroup : IGroup, IDisposable
where TObject : notnull
diff --git a/R3.DynamicData/Cache/Internal/SubscribeMany.cs b/R3Ext.DynamicData/Cache/Internal/SubscribeMany.cs
similarity index 99%
rename from R3.DynamicData/Cache/Internal/SubscribeMany.cs
rename to R3Ext.DynamicData/Cache/Internal/SubscribeMany.cs
index 66f9eb8..2c21b29 100644
--- a/R3.DynamicData/Cache/Internal/SubscribeMany.cs
+++ b/R3Ext.DynamicData/Cache/Internal/SubscribeMany.cs
@@ -1,5 +1,5 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
internal sealed class SubscribeMany
where TKey : notnull
diff --git a/R3.DynamicData/Cache/Internal/SuppressRefresh.cs b/R3Ext.DynamicData/Cache/Internal/SuppressRefresh.cs
similarity index 97%
rename from R3.DynamicData/Cache/Internal/SuppressRefresh.cs
rename to R3Ext.DynamicData/Cache/Internal/SuppressRefresh.cs
index 0254822..7bd77d1 100644
--- a/R3.DynamicData/Cache/Internal/SuppressRefresh.cs
+++ b/R3Ext.DynamicData/Cache/Internal/SuppressRefresh.cs
@@ -1,7 +1,7 @@
// Port of DynamicData SuppressRefresh to R3.
// Style suppression for internal operator implementation.
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
internal sealed class SuppressRefresh
where TObject : notnull
diff --git a/R3.DynamicData/Cache/Internal/TransformOnObservable.cs b/R3Ext.DynamicData/Cache/Internal/TransformOnObservable.cs
similarity index 99%
rename from R3.DynamicData/Cache/Internal/TransformOnObservable.cs
rename to R3Ext.DynamicData/Cache/Internal/TransformOnObservable.cs
index 928ce31..f88571c 100644
--- a/R3.DynamicData/Cache/Internal/TransformOnObservable.cs
+++ b/R3Ext.DynamicData/Cache/Internal/TransformOnObservable.cs
@@ -1,5 +1,5 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
internal sealed class TransformOnObservable
where TObject : notnull
diff --git a/R3.DynamicData/Cache/Internal/TreeBuilder.cs b/R3Ext.DynamicData/Cache/Internal/TreeBuilder.cs
similarity index 98%
rename from R3.DynamicData/Cache/Internal/TreeBuilder.cs
rename to R3Ext.DynamicData/Cache/Internal/TreeBuilder.cs
index 8276ace..1d23d3c 100644
--- a/R3.DynamicData/Cache/Internal/TreeBuilder.cs
+++ b/R3Ext.DynamicData/Cache/Internal/TreeBuilder.cs
@@ -8,10 +8,10 @@
using System;
using System.Linq;
using R3;
-using R3.DynamicData.Kernel;
-using R3.DynamicData.Operators;
+using R3Ext.DynamicData.Kernel;
+using R3Ext.DynamicData.Operators;
-namespace R3.DynamicData.Cache.Internal;
+namespace R3Ext.DynamicData.Cache.Internal;
///
/// Internal class that builds and maintains a tree structure from a flat changeset.
diff --git a/R3.DynamicData/Cache/Node.cs b/R3Ext.DynamicData/Cache/Node.cs
similarity index 98%
rename from R3.DynamicData/Cache/Node.cs
rename to R3Ext.DynamicData/Cache/Node.cs
index 6194465..9ae9900 100644
--- a/R3.DynamicData/Cache/Node.cs
+++ b/R3Ext.DynamicData/Cache/Node.cs
@@ -2,9 +2,9 @@
// Copyright (c) 2011-2023 Roland Pheasant. All rights reserved.
// Roland Pheasant licenses this file to you under the MIT license.
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Node describing the relationship between an item and its ancestors and descendants.
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.Batch.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.Batch.cs
similarity index 99%
rename from R3.DynamicData/Cache/ObservableCacheEx.Batch.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.Batch.cs
index d9e47bd..1f8c039 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.Batch.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.Batch.cs
@@ -5,7 +5,7 @@
using System;
using System.Collections.Generic;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extension methods for observable cache change sets.
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.ChangeKey.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.ChangeKey.cs
similarity index 93%
rename from R3.DynamicData/Cache/ObservableCacheEx.ChangeKey.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.ChangeKey.cs
index 9b84e66..62e2875 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.ChangeKey.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.ChangeKey.cs
@@ -1,9 +1,9 @@
// Public extension for ChangeKey operator.
using System;
-using R3.DynamicData.Cache.Internal;
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Cache.Internal;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extension methods for cache change key operations.
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.Convenience.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.Convenience.cs
similarity index 98%
rename from R3.DynamicData/Cache/ObservableCacheEx.Convenience.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.Convenience.cs
index af16c6c..b9aae11 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.Convenience.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.Convenience.cs
@@ -1,7 +1,7 @@
// Additional convenience extensions for SourceCache operations.
// Phase 1: expose predicate-based removals and batching helpers matching internal updater capabilities.
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
/// Extension methods for convenient cache operations.
public static partial class ObservableCacheEx
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.Filter.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.Filter.cs
similarity index 98%
rename from R3.DynamicData/Cache/ObservableCacheEx.Filter.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.Filter.cs
index f30d23c..59bf22f 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.Filter.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.Filter.cs
@@ -3,9 +3,9 @@
// - DD #1013: No bogus no-predicate overload exists here; only FilterCacheInternal which requires a predicate. Not affected.
// - DD #1048: ChangeReason.Refresh is handled: re-evaluates predicate and emits Refresh (still passes),
// Remove (stops passing), or Refresh (starts passing). Implementation is correct.
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extension methods for cache filtering operations.
@@ -20,7 +20,7 @@ public static partial class ObservableCacheEx
/// The source observable cache.
/// The predicate to filter items.
/// An observable that emits filtered change sets.
- /// Renamed to avoid ambiguity with R3.DynamicData.Operators.FilterOperator.Filter.
+ /// Renamed to avoid ambiguity with R3Ext.DynamicData.Operators.FilterOperator.Filter.
public static Observable> FilterCacheInternal(
this Observable> source,
Func predicate)
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.Grouping.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.Grouping.cs
similarity index 98%
rename from R3.DynamicData/Cache/ObservableCacheEx.Grouping.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.Grouping.cs
index e28ce9c..a63d629 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.Grouping.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.Grouping.cs
@@ -1,4 +1,4 @@
-// Upgraded Group operator implementation for R3.DynamicData
+// Upgraded Group operator implementation for R3Ext.DynamicData
// Audited against DD #938 (GroupOnObservable OnCompleted handling fix).
// The Subscribe call passes observer.OnCompleted as the completion handler, so when the source
// completes the downstream observer is completed correctly. Not affected by DD #938.
@@ -6,11 +6,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using R3.DynamicData.Cache.Internal;
-using R3.DynamicData.Kernel;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.Cache.Internal;
+using R3Ext.DynamicData.Kernel;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extension methods for observable cache change sets.
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.Joins.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.Joins.cs
similarity index 99%
rename from R3.DynamicData/Cache/ObservableCacheEx.Joins.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.Joins.cs
index e479e53..e44f788 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.Joins.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.Joins.cs
@@ -8,9 +8,9 @@
// then calling RecomputeAndEmit(), which removes results for stale keys and adds results for new
// overlapping keys. Foreign key changes are handled correctly.
// Provides diff emission (Add/Update/Remove) for joined pairs. Future work: Left/Right/Full joins and Many joins.
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extension methods for observable cache change sets.
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.Phase2.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.Phase2.cs
similarity index 99%
rename from R3.DynamicData/Cache/ObservableCacheEx.Phase2.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.Phase2.cs
index d9f197d..c0d6649 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.Phase2.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.Phase2.cs
@@ -2,11 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using R3;
-using R3.DynamicData.Cache;
-using R3.DynamicData.Kernel;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.Kernel;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
// Phase 2 cache operators for R3 port.
// NOTE: Adapted from DynamicData concepts; simplified to match R3's Observable (not System.IObservable). Inner observable completions NEVER complete outer streams.
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.Sort.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.Sort.cs
similarity index 99%
rename from R3.DynamicData/Cache/ObservableCacheEx.Sort.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.Sort.cs
index c968abf..80ed463 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.Sort.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.Sort.cs
@@ -1,9 +1,9 @@
// Port of DynamicData to R3.
-using R3.DynamicData.Kernel;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.Kernel;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extension methods for observable cache change sets.
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.SortAndBind.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.SortAndBind.cs
similarity index 98%
rename from R3.DynamicData/Cache/ObservableCacheEx.SortAndBind.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.SortAndBind.cs
index f9339ea..c96fb9d 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.SortAndBind.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.SortAndBind.cs
@@ -4,12 +4,12 @@
// full reset on the first changeset when the flag is set, regardless of ResetThreshold.
// Provides lower allocation and avoids transmitting full sorted state via intermediate sorted change sets.
using System.Collections.ObjectModel;
-using R3.DynamicData.Binding;
-using R3.DynamicData.Kernel;
-using R3.DynamicData.List;
-using R3.DynamicData.List.Internal;
+using R3Ext.DynamicData.Binding;
+using R3Ext.DynamicData.Kernel;
+using R3Ext.DynamicData.List;
+using R3Ext.DynamicData.List.Internal;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extension methods for observable cache change sets.
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.TransformAsync.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.TransformAsync.cs
similarity index 99%
rename from R3.DynamicData/Cache/ObservableCacheEx.TransformAsync.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.TransformAsync.cs
index e0c19cf..dcff5e4 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.TransformAsync.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.TransformAsync.cs
@@ -6,7 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
/// Async transformation extension methods for observable caches.
public static partial class ObservableCacheEx
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.TransformOnObservable.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.TransformOnObservable.cs
similarity index 97%
rename from R3.DynamicData/Cache/ObservableCacheEx.TransformOnObservable.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.TransformOnObservable.cs
index fcfb119..bd739db 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.TransformOnObservable.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.TransformOnObservable.cs
@@ -1,5 +1,5 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extension methods for observable cache change sets.
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.TransformSafe.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.TransformSafe.cs
similarity index 99%
rename from R3.DynamicData/Cache/ObservableCacheEx.TransformSafe.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.TransformSafe.cs
index 3f193ea..77d52f0 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.TransformSafe.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.TransformSafe.cs
@@ -1,8 +1,8 @@
// Port of DynamicData TransformSafe to R3.
using System;
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extension methods for observable cache change sets.
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.Virtualize.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.Virtualize.cs
similarity index 99%
rename from R3.DynamicData/Cache/ObservableCacheEx.Virtualize.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.Virtualize.cs
index 4ca16c0..3470595 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.Virtualize.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.Virtualize.cs
@@ -1,9 +1,9 @@
// Port of DynamicData to R3.
using System.Collections.Generic;
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extension methods for observable cache change sets.
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.WhenValueChanged.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.WhenValueChanged.cs
similarity index 99%
rename from R3.DynamicData/Cache/ObservableCacheEx.WhenValueChanged.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.WhenValueChanged.cs
index 350218d..a8aba26 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.WhenValueChanged.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.WhenValueChanged.cs
@@ -10,10 +10,10 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
-using R3.DynamicData.Kernel;
using R3Ext;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extension methods for observable cache change sets.
diff --git a/R3.DynamicData/Cache/ObservableCacheEx.cs b/R3Ext.DynamicData/Cache/ObservableCacheEx.cs
similarity index 99%
rename from R3.DynamicData/Cache/ObservableCacheEx.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheEx.cs
index 56c893b..618e947 100644
--- a/R3.DynamicData/Cache/ObservableCacheEx.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheEx.cs
@@ -3,11 +3,11 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq.Expressions;
-using R3.DynamicData.Binding;
-using R3.DynamicData.Kernel;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.Binding;
+using R3Ext.DynamicData.Kernel;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Observable cache extension methods.
diff --git a/R3.DynamicData/Cache/ObservableCacheExTreeTransformation.cs b/R3Ext.DynamicData/Cache/ObservableCacheExTreeTransformation.cs
similarity index 96%
rename from R3.DynamicData/Cache/ObservableCacheExTreeTransformation.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheExTreeTransformation.cs
index 26e5e8a..8776d7d 100644
--- a/R3.DynamicData/Cache/ObservableCacheExTreeTransformation.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheExTreeTransformation.cs
@@ -2,10 +2,10 @@
using System;
using R3;
-using R3.DynamicData.Cache.Internal;
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Cache.Internal;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extension methods for tree transformation operators.
diff --git a/R3.DynamicData/Cache/ObservableCacheExtensions.cs b/R3Ext.DynamicData/Cache/ObservableCacheExtensions.cs
similarity index 99%
rename from R3.DynamicData/Cache/ObservableCacheExtensions.cs
rename to R3Ext.DynamicData/Cache/ObservableCacheExtensions.cs
index ed6af14..8397608 100644
--- a/R3.DynamicData/Cache/ObservableCacheExtensions.cs
+++ b/R3Ext.DynamicData/Cache/ObservableCacheExtensions.cs
@@ -3,9 +3,9 @@
// Roland Pheasant licenses this file to you under the MIT license.
using System.Collections.Concurrent;
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Extensions for creating observable caches.
diff --git a/R3.DynamicData/Cache/SortAndBindOptions.cs b/R3Ext.DynamicData/Cache/SortAndBindOptions.cs
similarity index 95%
rename from R3.DynamicData/Cache/SortAndBindOptions.cs
rename to R3Ext.DynamicData/Cache/SortAndBindOptions.cs
index 86159e5..439de70 100644
--- a/R3.DynamicData/Cache/SortAndBindOptions.cs
+++ b/R3Ext.DynamicData/Cache/SortAndBindOptions.cs
@@ -1,8 +1,8 @@
// Combined Sort + Bind operator for cache side (inspired by DynamicData SortAndBind).
-using R3.DynamicData.Binding;
+using R3Ext.DynamicData.Binding;
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// Options for SortAndBind optimization.
diff --git a/R3.DynamicData/Cache/SourceCache.cs b/R3Ext.DynamicData/Cache/SourceCache.cs
similarity index 99%
rename from R3.DynamicData/Cache/SourceCache.cs
rename to R3Ext.DynamicData/Cache/SourceCache.cs
index f72bce6..af1806b 100644
--- a/R3.DynamicData/Cache/SourceCache.cs
+++ b/R3Ext.DynamicData/Cache/SourceCache.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Cache;
+namespace R3Ext.DynamicData.Cache;
///
/// An observable cache that tracks changes to items with unique keys.
diff --git a/R3.DynamicData/IChangeSet.cs b/R3Ext.DynamicData/IChangeSet.cs
similarity index 97%
rename from R3.DynamicData/IChangeSet.cs
rename to R3Ext.DynamicData/IChangeSet.cs
index da0868b..418d3a4 100644
--- a/R3.DynamicData/IChangeSet.cs
+++ b/R3Ext.DynamicData/IChangeSet.cs
@@ -1,7 +1,7 @@
// This is a port of DynamicData to R3 for enhanced performance.
// Original DynamicData: Copyright (c) 2011-2023 Roland Pheasant. Licensed under the MIT license.
-namespace R3.DynamicData;
+namespace R3Ext.DynamicData;
///
/// Base interface representing a set of changes.
diff --git a/R3.DynamicData/Kernel/ChangeReason.cs b/R3Ext.DynamicData/Kernel/ChangeReason.cs
similarity index 94%
rename from R3.DynamicData/Kernel/ChangeReason.cs
rename to R3Ext.DynamicData/Kernel/ChangeReason.cs
index 52d1009..c922eb4 100644
--- a/R3.DynamicData/Kernel/ChangeReason.cs
+++ b/R3Ext.DynamicData/Kernel/ChangeReason.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Kernel;
+namespace R3Ext.DynamicData.Kernel;
///
/// The reason for a change in a collection.
diff --git a/R3.DynamicData/Kernel/Error.cs b/R3Ext.DynamicData/Kernel/Error.cs
similarity index 97%
rename from R3.DynamicData/Kernel/Error.cs
rename to R3Ext.DynamicData/Kernel/Error.cs
index f725722..6ed79dd 100644
--- a/R3.DynamicData/Kernel/Error.cs
+++ b/R3Ext.DynamicData/Kernel/Error.cs
@@ -1,5 +1,5 @@
// Port of DynamicData Error to R3.
-namespace R3.DynamicData.Kernel;
+namespace R3Ext.DynamicData.Kernel;
///
/// Container for an error that occurred during transformation with the source item and key.
diff --git a/R3.DynamicData/Kernel/Optional.cs b/R3Ext.DynamicData/Kernel/Optional.cs
similarity index 99%
rename from R3.DynamicData/Kernel/Optional.cs
rename to R3Ext.DynamicData/Kernel/Optional.cs
index c825c7f..d8da63a 100644
--- a/R3.DynamicData/Kernel/Optional.cs
+++ b/R3Ext.DynamicData/Kernel/Optional.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Kernel;
+namespace R3Ext.DynamicData.Kernel;
///
/// Represents an optional value that may or may not be present.
diff --git a/R3.DynamicData/List/Change.cs b/R3Ext.DynamicData/List/Change.cs
similarity index 99%
rename from R3.DynamicData/List/Change.cs
rename to R3Ext.DynamicData/List/Change.cs
index 2b22a6d..0c993a9 100644
--- a/R3.DynamicData/List/Change.cs
+++ b/R3Ext.DynamicData/List/Change.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Represents a single change to a list.
diff --git a/R3.DynamicData/List/ChangeAwareList.cs b/R3Ext.DynamicData/List/ChangeAwareList.cs
similarity index 99%
rename from R3.DynamicData/List/ChangeAwareList.cs
rename to R3Ext.DynamicData/List/ChangeAwareList.cs
index 79a7cc8..4f0ffe2 100644
--- a/R3.DynamicData/List/ChangeAwareList.cs
+++ b/R3Ext.DynamicData/List/ChangeAwareList.cs
@@ -2,7 +2,7 @@
using System.Collections;
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// A list which captures all changes made to it.
diff --git a/R3.DynamicData/List/ChangeSet.cs b/R3Ext.DynamicData/List/ChangeSet.cs
similarity index 99%
rename from R3.DynamicData/List/ChangeSet.cs
rename to R3Ext.DynamicData/List/ChangeSet.cs
index d7791d7..532b25e 100644
--- a/R3.DynamicData/List/ChangeSet.cs
+++ b/R3Ext.DynamicData/List/ChangeSet.cs
@@ -2,7 +2,7 @@
using System.Collections;
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Represents a collection of changes to a list.
diff --git a/R3.DynamicData/List/Group.cs b/R3Ext.DynamicData/List/Group.cs
similarity index 96%
rename from R3.DynamicData/List/Group.cs
rename to R3Ext.DynamicData/List/Group.cs
index c2de669..25f9063 100644
--- a/R3.DynamicData/List/Group.cs
+++ b/R3Ext.DynamicData/List/Group.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Represents a group of items with a common key.
diff --git a/R3.DynamicData/List/IChangeSet.cs b/R3Ext.DynamicData/List/IChangeSet.cs
similarity index 96%
rename from R3.DynamicData/List/IChangeSet.cs
rename to R3Ext.DynamicData/List/IChangeSet.cs
index b8ffc9d..bc8c87c 100644
--- a/R3.DynamicData/List/IChangeSet.cs
+++ b/R3Ext.DynamicData/List/IChangeSet.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// A collection of changes for a list.
diff --git a/R3.DynamicData/List/IExtendedList.cs b/R3Ext.DynamicData/List/IExtendedList.cs
similarity index 97%
rename from R3.DynamicData/List/IExtendedList.cs
rename to R3Ext.DynamicData/List/IExtendedList.cs
index b93a8b4..458657a 100644
--- a/R3.DynamicData/List/IExtendedList.cs
+++ b/R3Ext.DynamicData/List/IExtendedList.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Represents a list which supports range operations.
diff --git a/R3.DynamicData/List/ISourceList.cs b/R3Ext.DynamicData/List/ISourceList.cs
similarity index 99%
rename from R3.DynamicData/List/ISourceList.cs
rename to R3Ext.DynamicData/List/ISourceList.cs
index 69e89a0..0800c0b 100644
--- a/R3.DynamicData/List/ISourceList.cs
+++ b/R3Ext.DynamicData/List/ISourceList.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// A reactive list that publishes changes as an observable change set.
diff --git a/R3.DynamicData/List/Internal/AddKey.cs b/R3Ext.DynamicData/List/Internal/AddKey.cs
similarity index 98%
rename from R3.DynamicData/List/Internal/AddKey.cs
rename to R3Ext.DynamicData/List/Internal/AddKey.cs
index d3fee20..bb1fd35 100644
--- a/R3.DynamicData/List/Internal/AddKey.cs
+++ b/R3Ext.DynamicData/List/Internal/AddKey.cs
@@ -2,10 +2,10 @@
using System;
using System.Collections.Generic;
using R3;
-using R3.DynamicData.Cache;
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class AddKey
where TObject : notnull
diff --git a/R3.DynamicData/List/Internal/AsyncDisposeMany.cs b/R3Ext.DynamicData/List/Internal/AsyncDisposeMany.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/AsyncDisposeMany.cs
rename to R3Ext.DynamicData/List/Internal/AsyncDisposeMany.cs
index 3626e46..7908ceb 100644
--- a/R3.DynamicData/List/Internal/AsyncDisposeMany.cs
+++ b/R3Ext.DynamicData/List/Internal/AsyncDisposeMany.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class AsyncDisposeMany
where T : notnull, IAsyncDisposable
diff --git a/R3.DynamicData/List/Internal/AutoRefresh.cs b/R3Ext.DynamicData/List/Internal/AutoRefresh.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/AutoRefresh.cs
rename to R3Ext.DynamicData/List/Internal/AutoRefresh.cs
index 05d24e1..db4a287 100644
--- a/R3.DynamicData/List/Internal/AutoRefresh.cs
+++ b/R3Ext.DynamicData/List/Internal/AutoRefresh.cs
@@ -9,7 +9,7 @@
using System.Linq;
using R3;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class AutoRefresh
where TObject : notnull
diff --git a/R3.DynamicData/List/Internal/BufferIf.cs b/R3Ext.DynamicData/List/Internal/BufferIf.cs
similarity index 98%
rename from R3.DynamicData/List/Internal/BufferIf.cs
rename to R3Ext.DynamicData/List/Internal/BufferIf.cs
index 62a2b4a..4984614 100644
--- a/R3.DynamicData/List/Internal/BufferIf.cs
+++ b/R3Ext.DynamicData/List/Internal/BufferIf.cs
@@ -3,7 +3,7 @@
using System;
using R3;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class BufferIf(Observable> source, Observable pauseIfTrueSelector, bool initialPauseState = false, TimeSpan? timeOut = null)
where T : notnull
diff --git a/R3.DynamicData/List/Internal/Combiner.cs b/R3Ext.DynamicData/List/Internal/Combiner.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/Combiner.cs
rename to R3Ext.DynamicData/List/Internal/Combiner.cs
index f740a94..1f28077 100644
--- a/R3.DynamicData/List/Internal/Combiner.cs
+++ b/R3Ext.DynamicData/List/Internal/Combiner.cs
@@ -1,6 +1,6 @@
using R3;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal enum CombineOperator
{
diff --git a/R3.DynamicData/List/Internal/DeferUntilLoaded.cs b/R3Ext.DynamicData/List/Internal/DeferUntilLoaded.cs
similarity index 95%
rename from R3.DynamicData/List/Internal/DeferUntilLoaded.cs
rename to R3Ext.DynamicData/List/Internal/DeferUntilLoaded.cs
index 49637d7..59af786 100644
--- a/R3.DynamicData/List/Internal/DeferUntilLoaded.cs
+++ b/R3Ext.DynamicData/List/Internal/DeferUntilLoaded.cs
@@ -1,6 +1,6 @@
using R3;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class DeferUntilLoaded
where T : notnull
diff --git a/R3.DynamicData/List/Internal/DisposeMany.cs b/R3Ext.DynamicData/List/Internal/DisposeMany.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/DisposeMany.cs
rename to R3Ext.DynamicData/List/Internal/DisposeMany.cs
index 8e14148..c3eb1a2 100644
--- a/R3.DynamicData/List/Internal/DisposeMany.cs
+++ b/R3Ext.DynamicData/List/Internal/DisposeMany.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class DisposeMany
where T : notnull
diff --git a/R3.DynamicData/List/Internal/DistinctValues.cs b/R3Ext.DynamicData/List/Internal/DistinctValues.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/DistinctValues.cs
rename to R3Ext.DynamicData/List/Internal/DistinctValues.cs
index 5d3a10f..dbd5483 100644
--- a/R3.DynamicData/List/Internal/DistinctValues.cs
+++ b/R3Ext.DynamicData/List/Internal/DistinctValues.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class DistinctValues
where TValue : notnull
diff --git a/R3.DynamicData/List/Internal/DynamicFilter.cs b/R3Ext.DynamicData/List/Internal/DynamicFilter.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/DynamicFilter.cs
rename to R3Ext.DynamicData/List/Internal/DynamicFilter.cs
index 65ee3a4..910028a 100644
--- a/R3.DynamicData/List/Internal/DynamicFilter.cs
+++ b/R3Ext.DynamicData/List/Internal/DynamicFilter.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class DynamicFilter
{
diff --git a/R3.DynamicData/List/Internal/Filter.cs b/R3Ext.DynamicData/List/Internal/Filter.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/Filter.cs
rename to R3Ext.DynamicData/List/Internal/Filter.cs
index a5e537a..bc06e46 100644
--- a/R3.DynamicData/List/Internal/Filter.cs
+++ b/R3Ext.DynamicData/List/Internal/Filter.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class Filter
{
diff --git a/R3.DynamicData/List/Internal/GroupBy.cs b/R3Ext.DynamicData/List/Internal/GroupBy.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/GroupBy.cs
rename to R3Ext.DynamicData/List/Internal/GroupBy.cs
index 7a0de58..10f5af7 100644
--- a/R3.DynamicData/List/Internal/GroupBy.cs
+++ b/R3Ext.DynamicData/List/Internal/GroupBy.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class GroupBy
where TKey : notnull
diff --git a/R3.DynamicData/List/Internal/MergeMany.cs b/R3Ext.DynamicData/List/Internal/MergeMany.cs
similarity index 97%
rename from R3.DynamicData/List/Internal/MergeMany.cs
rename to R3Ext.DynamicData/List/Internal/MergeMany.cs
index e42ed54..a7fc967 100644
--- a/R3.DynamicData/List/Internal/MergeMany.cs
+++ b/R3Ext.DynamicData/List/Internal/MergeMany.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3.
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class MergeMany
where TSource : notnull
diff --git a/R3.DynamicData/List/Internal/OnBeingAdded.cs b/R3Ext.DynamicData/List/Internal/OnBeingAdded.cs
similarity index 95%
rename from R3.DynamicData/List/Internal/OnBeingAdded.cs
rename to R3Ext.DynamicData/List/Internal/OnBeingAdded.cs
index 7afc53a..7084d10 100644
--- a/R3.DynamicData/List/Internal/OnBeingAdded.cs
+++ b/R3Ext.DynamicData/List/Internal/OnBeingAdded.cs
@@ -1,6 +1,6 @@
using R3;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class OnBeingAdded
where T : notnull
diff --git a/R3.DynamicData/List/Internal/OnBeingRemoved.cs b/R3Ext.DynamicData/List/Internal/OnBeingRemoved.cs
similarity index 98%
rename from R3.DynamicData/List/Internal/OnBeingRemoved.cs
rename to R3Ext.DynamicData/List/Internal/OnBeingRemoved.cs
index 90c8b06..b2bcf02 100644
--- a/R3.DynamicData/List/Internal/OnBeingRemoved.cs
+++ b/R3Ext.DynamicData/List/Internal/OnBeingRemoved.cs
@@ -1,6 +1,6 @@
using R3;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class OnBeingRemoved
where T : notnull
diff --git a/R3.DynamicData/List/Internal/QueryWhenChanged.cs b/R3Ext.DynamicData/List/Internal/QueryWhenChanged.cs
similarity index 98%
rename from R3.DynamicData/List/Internal/QueryWhenChanged.cs
rename to R3Ext.DynamicData/List/Internal/QueryWhenChanged.cs
index 9e595da..e326d98 100644
--- a/R3.DynamicData/List/Internal/QueryWhenChanged.cs
+++ b/R3Ext.DynamicData/List/Internal/QueryWhenChanged.cs
@@ -1,6 +1,6 @@
using R3;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class QueryWhenChanged
where T : notnull
diff --git a/R3.DynamicData/List/Internal/RefCount.cs b/R3Ext.DynamicData/List/Internal/RefCount.cs
similarity index 88%
rename from R3.DynamicData/List/Internal/RefCount.cs
rename to R3Ext.DynamicData/List/Internal/RefCount.cs
index 6f26746..1b0434f 100644
--- a/R3.DynamicData/List/Internal/RefCount.cs
+++ b/R3Ext.DynamicData/List/Internal/RefCount.cs
@@ -1,6 +1,6 @@
using R3;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class RefCount
where T : notnull
diff --git a/R3.DynamicData/List/Internal/RemoveIndex.cs b/R3Ext.DynamicData/List/Internal/RemoveIndex.cs
similarity index 98%
rename from R3.DynamicData/List/Internal/RemoveIndex.cs
rename to R3Ext.DynamicData/List/Internal/RemoveIndex.cs
index 9b2597e..292b1aa 100644
--- a/R3.DynamicData/List/Internal/RemoveIndex.cs
+++ b/R3Ext.DynamicData/List/Internal/RemoveIndex.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class RemoveIndex
{
diff --git a/R3.DynamicData/List/Internal/Reverse.cs b/R3Ext.DynamicData/List/Internal/Reverse.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/Reverse.cs
rename to R3Ext.DynamicData/List/Internal/Reverse.cs
index 5452866..f339076 100644
--- a/R3.DynamicData/List/Internal/Reverse.cs
+++ b/R3Ext.DynamicData/List/Internal/Reverse.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class Reverse
{
diff --git a/R3.DynamicData/List/Internal/Sort.cs b/R3Ext.DynamicData/List/Internal/Sort.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/Sort.cs
rename to R3Ext.DynamicData/List/Internal/Sort.cs
index fb9626f..6a1285e 100644
--- a/R3.DynamicData/List/Internal/Sort.cs
+++ b/R3Ext.DynamicData/List/Internal/Sort.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class Sort
where T : notnull
diff --git a/R3.DynamicData/List/Internal/StatefulFilter.cs b/R3Ext.DynamicData/List/Internal/StatefulFilter.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/StatefulFilter.cs
rename to R3Ext.DynamicData/List/Internal/StatefulFilter.cs
index d4c9cbb..09129f4 100644
--- a/R3.DynamicData/List/Internal/StatefulFilter.cs
+++ b/R3Ext.DynamicData/List/Internal/StatefulFilter.cs
@@ -1,5 +1,5 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
// Audited DD #941: StatefulFilter avoids allocating a new predicate delegate on each state change
// by accepting the state as a separate observable and a (T, TState) โ bool predicate.
diff --git a/R3.DynamicData/List/Internal/SubscribeMany.cs b/R3Ext.DynamicData/List/Internal/SubscribeMany.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/SubscribeMany.cs
rename to R3Ext.DynamicData/List/Internal/SubscribeMany.cs
index 811186b..7383e03 100644
--- a/R3.DynamicData/List/Internal/SubscribeMany.cs
+++ b/R3Ext.DynamicData/List/Internal/SubscribeMany.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class SubscribeMany
where T : notnull
diff --git a/R3.DynamicData/List/Internal/Switch.cs b/R3Ext.DynamicData/List/Internal/Switch.cs
similarity index 61%
rename from R3.DynamicData/List/Internal/Switch.cs
rename to R3Ext.DynamicData/List/Internal/Switch.cs
index 9a39859..c321da3 100644
--- a/R3.DynamicData/List/Internal/Switch.cs
+++ b/R3Ext.DynamicData/List/Internal/Switch.cs
@@ -1,13 +1,15 @@
// Port of DynamicData to R3.
// Audited against DD #968 (Switch error propagation fix).
-// The inner source is subscribed directly with `innerSource.Subscribe(observer)`, which routes
-// OnNext, OnError, and OnCompleted from the inner source directly to the downstream observer.
-// Errors are therefore propagated correctly; this implementation is NOT affected by DD #968.
+// Each inner source is subscribed with a fresh forwarding observer (lambdas) rather than by
+// reusing the single downstream observer instance: R3 observers are single-subscription, so
+// reusing the same observer across successive inner sources throws on the second subscribe.
+// OnNext and OnErrorResume are forwarded downstream; inner OnCompleted is intentionally ignored
+// so the merged stream only completes when the outer source of sources completes.
using System;
using R3;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class Switch(Observable>> sources)
where T : notnull
@@ -26,7 +28,10 @@ public Observable> Run() => Observable.Create>(
{
lock (locker)
{
- currentSubscription.Disposable = innerSource.Subscribe(observer);
+ currentSubscription.Disposable = innerSource.Subscribe(
+ value => observer.OnNext(value),
+ error => observer.OnErrorResume(error),
+ static _ => { });
}
},
observer.OnErrorResume,
diff --git a/R3.DynamicData/List/Internal/ToObservableChangeSet.cs b/R3Ext.DynamicData/List/Internal/ToObservableChangeSet.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/ToObservableChangeSet.cs
rename to R3Ext.DynamicData/List/Internal/ToObservableChangeSet.cs
index 05666f8..141cf7a 100644
--- a/R3.DynamicData/List/Internal/ToObservableChangeSet.cs
+++ b/R3Ext.DynamicData/List/Internal/ToObservableChangeSet.cs
@@ -9,7 +9,7 @@
using System.Linq;
using R3;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class ToObservableChangeSet
where TObject : notnull
diff --git a/R3.DynamicData/List/Internal/Transform.cs b/R3Ext.DynamicData/List/Internal/Transform.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/Transform.cs
rename to R3Ext.DynamicData/List/Internal/Transform.cs
index fab8419..56fc3fe 100644
--- a/R3.DynamicData/List/Internal/Transform.cs
+++ b/R3Ext.DynamicData/List/Internal/Transform.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class Transform
{
diff --git a/R3.DynamicData/List/Internal/TransformMany.cs b/R3Ext.DynamicData/List/Internal/TransformMany.cs
similarity index 99%
rename from R3.DynamicData/List/Internal/TransformMany.cs
rename to R3Ext.DynamicData/List/Internal/TransformMany.cs
index be618df..e24d4bb 100644
--- a/R3.DynamicData/List/Internal/TransformMany.cs
+++ b/R3Ext.DynamicData/List/Internal/TransformMany.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class TransformMany
where TDestination : notnull
diff --git a/R3.DynamicData/List/Internal/Virtualiser.cs b/R3Ext.DynamicData/List/Internal/Virtualiser.cs
similarity index 98%
rename from R3.DynamicData/List/Internal/Virtualiser.cs
rename to R3Ext.DynamicData/List/Internal/Virtualiser.cs
index 16e6f87..4637916 100644
--- a/R3.DynamicData/List/Internal/Virtualiser.cs
+++ b/R3Ext.DynamicData/List/Internal/Virtualiser.cs
@@ -1,6 +1,6 @@
using R3;
-namespace R3.DynamicData.List.Internal;
+namespace R3Ext.DynamicData.List.Internal;
internal sealed class Virtualiser
where T : notnull
diff --git a/R3.DynamicData/List/ListChangeReason.cs b/R3Ext.DynamicData/List/ListChangeReason.cs
similarity index 95%
rename from R3.DynamicData/List/ListChangeReason.cs
rename to R3Ext.DynamicData/List/ListChangeReason.cs
index bf6a218..3cc5acf 100644
--- a/R3.DynamicData/List/ListChangeReason.cs
+++ b/R3Ext.DynamicData/List/ListChangeReason.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// The reason for a change in a list.
diff --git a/R3.DynamicData/List/ObservableListAggregates.cs b/R3Ext.DynamicData/List/ObservableListAggregates.cs
similarity index 99%
rename from R3.DynamicData/List/ObservableListAggregates.cs
rename to R3Ext.DynamicData/List/ObservableListAggregates.cs
index 1bb732d..a463d0f 100644
--- a/R3.DynamicData/List/ObservableListAggregates.cs
+++ b/R3Ext.DynamicData/List/ObservableListAggregates.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3.
-using R3.DynamicData.Utilities;
+using R3Ext.DynamicData.Utilities;
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Aggregate operators for observable lists.
diff --git a/R3.DynamicData/List/ObservableListEx.CastConvert.cs b/R3Ext.DynamicData/List/ObservableListEx.CastConvert.cs
similarity index 96%
rename from R3.DynamicData/List/ObservableListEx.CastConvert.cs
rename to R3Ext.DynamicData/List/ObservableListEx.CastConvert.cs
index 62ab3f8..784ac3c 100644
--- a/R3.DynamicData/List/ObservableListEx.CastConvert.cs
+++ b/R3Ext.DynamicData/List/ObservableListEx.CastConvert.cs
@@ -1,5 +1,5 @@
// Convenience Convert operator for list change sets.
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Extension methods for observable list change sets.
diff --git a/R3.DynamicData/List/ObservableListEx.FilterOnObservable.cs b/R3Ext.DynamicData/List/ObservableListEx.FilterOnObservable.cs
similarity index 99%
rename from R3.DynamicData/List/ObservableListEx.FilterOnObservable.cs
rename to R3Ext.DynamicData/List/ObservableListEx.FilterOnObservable.cs
index e396695..9ac5b2f 100644
--- a/R3.DynamicData/List/ObservableListEx.FilterOnObservable.cs
+++ b/R3Ext.DynamicData/List/ObservableListEx.FilterOnObservable.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Extension methods for observable list change sets.
diff --git a/R3.DynamicData/List/ObservableListEx.LimitSizeTo.cs b/R3Ext.DynamicData/List/ObservableListEx.LimitSizeTo.cs
similarity index 99%
rename from R3.DynamicData/List/ObservableListEx.LimitSizeTo.cs
rename to R3Ext.DynamicData/List/ObservableListEx.LimitSizeTo.cs
index 96805e6..60a7106 100644
--- a/R3.DynamicData/List/ObservableListEx.LimitSizeTo.cs
+++ b/R3Ext.DynamicData/List/ObservableListEx.LimitSizeTo.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Eviction policy for .
diff --git a/R3.DynamicData/List/ObservableListEx.MergeChangeSets.cs b/R3Ext.DynamicData/List/ObservableListEx.MergeChangeSets.cs
similarity index 99%
rename from R3.DynamicData/List/ObservableListEx.MergeChangeSets.cs
rename to R3Ext.DynamicData/List/ObservableListEx.MergeChangeSets.cs
index 68ad927..02d62e5 100644
--- a/R3.DynamicData/List/ObservableListEx.MergeChangeSets.cs
+++ b/R3Ext.DynamicData/List/ObservableListEx.MergeChangeSets.cs
@@ -6,7 +6,7 @@
using System.Collections.Generic;
using System.Linq;
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Extension methods for observable list change sets.
diff --git a/R3.DynamicData/List/ObservableListEx.TransformAsync.cs b/R3Ext.DynamicData/List/ObservableListEx.TransformAsync.cs
similarity index 99%
rename from R3.DynamicData/List/ObservableListEx.TransformAsync.cs
rename to R3Ext.DynamicData/List/ObservableListEx.TransformAsync.cs
index c7ce329..0cd45a2 100644
--- a/R3.DynamicData/List/ObservableListEx.TransformAsync.cs
+++ b/R3Ext.DynamicData/List/ObservableListEx.TransformAsync.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Extension methods for observable list change sets.
diff --git a/R3.DynamicData/List/ObservableListEx.Virtualize.cs b/R3Ext.DynamicData/List/ObservableListEx.Virtualize.cs
similarity index 99%
rename from R3.DynamicData/List/ObservableListEx.Virtualize.cs
rename to R3Ext.DynamicData/List/ObservableListEx.Virtualize.cs
index 803e5c9..a168912 100644
--- a/R3.DynamicData/List/ObservableListEx.Virtualize.cs
+++ b/R3Ext.DynamicData/List/ObservableListEx.Virtualize.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3.
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Extension methods for observable list change sets.
diff --git a/R3.DynamicData/List/ObservableListEx.cs b/R3Ext.DynamicData/List/ObservableListEx.cs
similarity index 99%
rename from R3.DynamicData/List/ObservableListEx.cs
rename to R3Ext.DynamicData/List/ObservableListEx.cs
index 45311d9..9bc4f3b 100644
--- a/R3.DynamicData/List/ObservableListEx.cs
+++ b/R3Ext.DynamicData/List/ObservableListEx.cs
@@ -3,11 +3,11 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq.Expressions;
-using R3.DynamicData.Binding;
-using R3.DynamicData.Cache;
-using R3.DynamicData.List.Internal;
+using R3Ext.DynamicData.Binding;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.List.Internal;
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Extension methods for observable list change sets.
@@ -22,7 +22,7 @@ public static partial class ObservableListEx
/// The source observable change set.
/// Function to transform each item.
/// An observable that emits change sets with transformed items.
- public static Observable> Transform(
+ public static Observable> Transform(
this Observable> source,
Func selector)
{
diff --git a/R3.DynamicData/List/SortOptions.cs b/R3Ext.DynamicData/List/SortOptions.cs
similarity index 94%
rename from R3.DynamicData/List/SortOptions.cs
rename to R3Ext.DynamicData/List/SortOptions.cs
index 75fc173..3ab8601 100644
--- a/R3.DynamicData/List/SortOptions.cs
+++ b/R3Ext.DynamicData/List/SortOptions.cs
@@ -4,7 +4,7 @@
using System;
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// Sort optimization options.
diff --git a/R3.DynamicData/List/SourceList.cs b/R3Ext.DynamicData/List/SourceList.cs
similarity index 99%
rename from R3.DynamicData/List/SourceList.cs
rename to R3Ext.DynamicData/List/SourceList.cs
index d9a7e2d..3c15a33 100644
--- a/R3.DynamicData/List/SourceList.cs
+++ b/R3Ext.DynamicData/List/SourceList.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.List;
+namespace R3Ext.DynamicData.List;
///
/// A reactive list implementation that publishes changes as an observable change set.
diff --git a/R3.DynamicData/Operators/CombineOperator.cs b/R3Ext.DynamicData/Operators/CombineOperator.cs
similarity index 98%
rename from R3.DynamicData/Operators/CombineOperator.cs
rename to R3Ext.DynamicData/Operators/CombineOperator.cs
index 3abcc29..89eff79 100644
--- a/R3.DynamicData/Operators/CombineOperator.cs
+++ b/R3Ext.DynamicData/Operators/CombineOperator.cs
@@ -1,9 +1,9 @@
// Port of DynamicData to R3.
-using R3.DynamicData.Cache;
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.Kernel;
-namespace R3.DynamicData.Operators;
+namespace R3Ext.DynamicData.Operators;
///
/// Extension methods for combining multiple observable cache change sets.
diff --git a/R3.DynamicData/Operators/FilterOperator.cs b/R3Ext.DynamicData/Operators/FilterOperator.cs
similarity index 99%
rename from R3.DynamicData/Operators/FilterOperator.cs
rename to R3Ext.DynamicData/Operators/FilterOperator.cs
index c5eb21b..4f13c20 100644
--- a/R3.DynamicData/Operators/FilterOperator.cs
+++ b/R3Ext.DynamicData/Operators/FilterOperator.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3.
-using R3.DynamicData.Cache;
+using R3Ext.DynamicData.Cache;
-namespace R3.DynamicData.Operators;
+namespace R3Ext.DynamicData.Operators;
///
/// Extension methods for filtering observable change sets.
diff --git a/R3.DynamicData/Operators/TransformOperator.cs b/R3Ext.DynamicData/Operators/TransformOperator.cs
similarity index 99%
rename from R3.DynamicData/Operators/TransformOperator.cs
rename to R3Ext.DynamicData/Operators/TransformOperator.cs
index 3c030b4..4fa6338 100644
--- a/R3.DynamicData/Operators/TransformOperator.cs
+++ b/R3Ext.DynamicData/Operators/TransformOperator.cs
@@ -1,8 +1,8 @@
// Port of DynamicData to R3.
-using R3.DynamicData.Cache;
+using R3Ext.DynamicData.Cache;
-namespace R3.DynamicData.Operators;
+namespace R3Ext.DynamicData.Operators;
///
/// Extension methods for transforming observable change sets.
diff --git a/R3.DynamicData/R3.DynamicData.csproj b/R3Ext.DynamicData/R3Ext.DynamicData.csproj
similarity index 72%
rename from R3.DynamicData/R3.DynamicData.csproj
rename to R3Ext.DynamicData/R3Ext.DynamicData.csproj
index e7c9a84..a0808be 100644
--- a/R3.DynamicData/R3.DynamicData.csproj
+++ b/R3Ext.DynamicData/R3Ext.DynamicData.csproj
@@ -6,8 +6,10 @@
enable
true
true
- R3.DynamicData
- R3 Dynamic Data
+ R3Ext.DynamicData
+ R3Ext.DynamicData
+ R3Ext.DynamicData
+ R3Ext Dynamic Data
DynamicData ported to R3 - Reactive collections for high-performance scenarios. API-compatible with DynamicData but powered by R3 for superior performance.
reactive;rx;r3;dynamicdata;collections;observable;linq
Copyright (c) 2025
@@ -18,6 +20,12 @@
+
+
+
+
diff --git a/R3.DynamicData/Utilities/RefBool.cs b/R3Ext.DynamicData/Utilities/RefBool.cs
similarity index 96%
rename from R3.DynamicData/Utilities/RefBool.cs
rename to R3Ext.DynamicData/Utilities/RefBool.cs
index 5e34ba9..8a4c403 100644
--- a/R3.DynamicData/Utilities/RefBool.cs
+++ b/R3Ext.DynamicData/Utilities/RefBool.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Utilities;
+namespace R3Ext.DynamicData.Utilities;
///
/// A mutable boolean wrapper for use in closure-free state structs.
diff --git a/R3.DynamicData/Utilities/RefInt.cs b/R3Ext.DynamicData/Utilities/RefInt.cs
similarity index 97%
rename from R3.DynamicData/Utilities/RefInt.cs
rename to R3Ext.DynamicData/Utilities/RefInt.cs
index 1d60c75..d5ec906 100644
--- a/R3.DynamicData/Utilities/RefInt.cs
+++ b/R3Ext.DynamicData/Utilities/RefInt.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Utilities;
+namespace R3Ext.DynamicData.Utilities;
///
/// A mutable integer wrapper for use in closure-free state structs.
diff --git a/R3.DynamicData/Utilities/RefValue.cs b/R3Ext.DynamicData/Utilities/RefValue.cs
similarity index 95%
rename from R3.DynamicData/Utilities/RefValue.cs
rename to R3Ext.DynamicData/Utilities/RefValue.cs
index 3c9c1bf..f64765a 100644
--- a/R3.DynamicData/Utilities/RefValue.cs
+++ b/R3Ext.DynamicData/Utilities/RefValue.cs
@@ -1,6 +1,6 @@
// Port of DynamicData to R3.
-namespace R3.DynamicData.Utilities;
+namespace R3Ext.DynamicData.Utilities;
///
/// A mutable generic value wrapper for use in closure-free state structs.
diff --git a/R3.DynamicData/Utilities/VirtualRequest.cs b/R3Ext.DynamicData/Utilities/VirtualRequest.cs
similarity index 98%
rename from R3.DynamicData/Utilities/VirtualRequest.cs
rename to R3Ext.DynamicData/Utilities/VirtualRequest.cs
index 03aa680..5a83221 100644
--- a/R3.DynamicData/Utilities/VirtualRequest.cs
+++ b/R3Ext.DynamicData/Utilities/VirtualRequest.cs
@@ -1,4 +1,4 @@
-namespace R3.DynamicData;
+namespace R3Ext.DynamicData;
///
/// Represents a request for a virtual page of data.
diff --git a/R3Ext.SampleApp/Pages/DDAggregationPage.xaml.cs b/R3Ext.SampleApp/Pages/DDAggregationPage.xaml.cs
index 594e7af..d36026c 100644
--- a/R3Ext.SampleApp/Pages/DDAggregationPage.xaml.cs
+++ b/R3Ext.SampleApp/Pages/DDAggregationPage.xaml.cs
@@ -3,7 +3,7 @@
using System.Linq;
using Microsoft.Maui.Controls;
using R3;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
namespace R3Ext.SampleApp;
diff --git a/R3Ext.SampleApp/Pages/DDGroupingPage.xaml.cs b/R3Ext.SampleApp/Pages/DDGroupingPage.xaml.cs
index e1cf127..449ba22 100644
--- a/R3Ext.SampleApp/Pages/DDGroupingPage.xaml.cs
+++ b/R3Ext.SampleApp/Pages/DDGroupingPage.xaml.cs
@@ -3,7 +3,7 @@
using System.Linq;
using Microsoft.Maui.Controls;
using R3;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
namespace R3Ext.SampleApp;
diff --git a/R3Ext.SampleApp/Pages/DDLifecyclePage.xaml.cs b/R3Ext.SampleApp/Pages/DDLifecyclePage.xaml.cs
index 44734d1..45f51e5 100644
--- a/R3Ext.SampleApp/Pages/DDLifecyclePage.xaml.cs
+++ b/R3Ext.SampleApp/Pages/DDLifecyclePage.xaml.cs
@@ -4,7 +4,7 @@
using System.Text;
using Microsoft.Maui.Controls;
using R3;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
namespace R3Ext.SampleApp;
diff --git a/R3Ext.SampleApp/Pages/DDLogicalOperatorsPage.xaml.cs b/R3Ext.SampleApp/Pages/DDLogicalOperatorsPage.xaml.cs
index 57b4367..04bafe4 100644
--- a/R3Ext.SampleApp/Pages/DDLogicalOperatorsPage.xaml.cs
+++ b/R3Ext.SampleApp/Pages/DDLogicalOperatorsPage.xaml.cs
@@ -3,7 +3,7 @@
using System.Linq;
using Microsoft.Maui.Controls;
using R3;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
namespace R3Ext.SampleApp;
diff --git a/R3Ext.SampleApp/Pages/DDRefreshPage.xaml.cs b/R3Ext.SampleApp/Pages/DDRefreshPage.xaml.cs
index 5235c7d..b405cb9 100644
--- a/R3Ext.SampleApp/Pages/DDRefreshPage.xaml.cs
+++ b/R3Ext.SampleApp/Pages/DDRefreshPage.xaml.cs
@@ -5,7 +5,7 @@
using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls;
using R3;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
namespace R3Ext.SampleApp;
diff --git a/R3Ext.SampleApp/Pages/DDTransformationPage.xaml.cs b/R3Ext.SampleApp/Pages/DDTransformationPage.xaml.cs
index b56672f..125b3fd 100644
--- a/R3Ext.SampleApp/Pages/DDTransformationPage.xaml.cs
+++ b/R3Ext.SampleApp/Pages/DDTransformationPage.xaml.cs
@@ -5,7 +5,7 @@
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using R3;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
using R3Ext.SampleApp.ViewModels;
namespace R3Ext.SampleApp;
diff --git a/R3Ext.SampleApp/Pages/DynamicDataBasicsPage.xaml.cs b/R3Ext.SampleApp/Pages/DynamicDataBasicsPage.xaml.cs
index 04fb44b..e9743e5 100644
--- a/R3Ext.SampleApp/Pages/DynamicDataBasicsPage.xaml.cs
+++ b/R3Ext.SampleApp/Pages/DynamicDataBasicsPage.xaml.cs
@@ -4,7 +4,7 @@
using System.Linq;
using Microsoft.Maui.Controls;
using R3;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
namespace R3Ext.SampleApp;
diff --git a/R3Ext.SampleApp/Pages/DynamicDataCachePage.xaml.cs b/R3Ext.SampleApp/Pages/DynamicDataCachePage.xaml.cs
index 9bcca21..de648c9 100644
--- a/R3Ext.SampleApp/Pages/DynamicDataCachePage.xaml.cs
+++ b/R3Ext.SampleApp/Pages/DynamicDataCachePage.xaml.cs
@@ -3,7 +3,7 @@
using System.Linq;
using Microsoft.Maui.Controls;
using R3;
-using R3.DynamicData.Cache;
+using R3Ext.DynamicData.Cache;
using R3Ext.SampleApp.ViewModels;
namespace R3Ext.SampleApp;
@@ -141,9 +141,9 @@ private void OnStartWatching(object sender, EventArgs e)
{
var action = change.Reason switch
{
- R3.DynamicData.Kernel.ChangeReason.Add => "Added",
- R3.DynamicData.Kernel.ChangeReason.Update => "Updated",
- R3.DynamicData.Kernel.ChangeReason.Remove => "Removed",
+ R3Ext.DynamicData.Kernel.ChangeReason.Add => "Added",
+ R3Ext.DynamicData.Kernel.ChangeReason.Update => "Updated",
+ R3Ext.DynamicData.Kernel.ChangeReason.Remove => "Removed",
_ => "Changed",
};
this.WatchLabel.Text = $"ID {id} {action}: {change.Current.Name} ({change.Current.City})";
diff --git a/R3Ext.SampleApp/Pages/DynamicDataDistinctValuesPage.xaml.cs b/R3Ext.SampleApp/Pages/DynamicDataDistinctValuesPage.xaml.cs
index b9e758b..c621728 100644
--- a/R3Ext.SampleApp/Pages/DynamicDataDistinctValuesPage.xaml.cs
+++ b/R3Ext.SampleApp/Pages/DynamicDataDistinctValuesPage.xaml.cs
@@ -4,8 +4,8 @@
using System.Linq;
using Microsoft.Maui.Controls;
using R3;
-using R3.DynamicData.Cache;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.List;
namespace R3Ext.SampleApp;
diff --git a/R3Ext.SampleApp/Pages/DynamicDataFilterSortPage.xaml.cs b/R3Ext.SampleApp/Pages/DynamicDataFilterSortPage.xaml.cs
index 6ffd202..ae992f6 100644
--- a/R3Ext.SampleApp/Pages/DynamicDataFilterSortPage.xaml.cs
+++ b/R3Ext.SampleApp/Pages/DynamicDataFilterSortPage.xaml.cs
@@ -4,7 +4,7 @@
using System.Linq;
using Microsoft.Maui.Controls;
using R3;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
using R3Ext.SampleApp.ViewModels;
namespace R3Ext.SampleApp;
@@ -46,7 +46,7 @@ public DynamicDataFilterSortPage()
_comparerSubject.OnNext(NameAscComparer);
// Prepare target collection
- var target = new R3.DynamicData.Binding.ObservableCollectionExtended();
+ var target = new R3Ext.DynamicData.Binding.ObservableCollectionExtended();
_people = new ReadOnlyObservableCollection(target);
// Pipeline: AutoRefresh on Name and Age changes -> Filter -> Sort -> Bind
diff --git a/R3Ext.SampleApp/Pages/DynamicDataOperatorsPage.xaml.cs b/R3Ext.SampleApp/Pages/DynamicDataOperatorsPage.xaml.cs
index bdce567..0f386c2 100644
--- a/R3Ext.SampleApp/Pages/DynamicDataOperatorsPage.xaml.cs
+++ b/R3Ext.SampleApp/Pages/DynamicDataOperatorsPage.xaml.cs
@@ -4,7 +4,7 @@
using System.Linq;
using Microsoft.Maui.Controls;
using R3;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.List;
using R3Ext.SampleApp.ViewModels;
namespace R3Ext.SampleApp;
diff --git a/R3Ext.SampleApp/Pages/DynamicDataTransformManyPage.xaml.cs b/R3Ext.SampleApp/Pages/DynamicDataTransformManyPage.xaml.cs
index 6f8b6e7..19c83ce 100644
--- a/R3Ext.SampleApp/Pages/DynamicDataTransformManyPage.xaml.cs
+++ b/R3Ext.SampleApp/Pages/DynamicDataTransformManyPage.xaml.cs
@@ -4,8 +4,8 @@
using System.Linq;
using Microsoft.Maui.Controls;
using R3;
-using R3.DynamicData.Cache;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.List;
using R3Ext.SampleApp.ViewModels;
namespace R3Ext.SampleApp;
diff --git a/R3Ext.SampleApp/R3Ext.SampleApp.csproj b/R3Ext.SampleApp/R3Ext.SampleApp.csproj
index 081d34d..dc8b6dd 100644
--- a/R3Ext.SampleApp/R3Ext.SampleApp.csproj
+++ b/R3Ext.SampleApp/R3Ext.SampleApp.csproj
@@ -53,7 +53,7 @@ either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
-
+
diff --git a/R3Ext.Tests/AggregateStreamTests.cs b/R3Ext.Tests/AggregateStreamTests.cs
new file mode 100644
index 0000000..23da619
--- /dev/null
+++ b/R3Ext.Tests/AggregateStreamTests.cs
@@ -0,0 +1,107 @@
+#pragma warning disable SA1107, SA1124, SA1501, SA1503, SA1515, SA1025, SA1520, SA1513, SA1508, SA1516
+using R3;
+using R3.Collections;
+
+namespace R3Ext.Tests;
+
+public class AggregateStreamTests
+{
+ [Fact]
+ public void RunningCount_EmitsIncrementalCount()
+ {
+ Subject subject = new();
+ LiveList result = subject.RunningCount().ToLiveList();
+ subject.OnNext("a"); subject.OnNext("b"); subject.OnNext("c");
+ subject.OnCompleted();
+ Assert.Equal(new[] { 1, 2, 3 }, result.ToArray());
+ }
+
+ [Fact]
+ public void RunningSum_EmitsRunningSumOfInts()
+ {
+ Subject subject = new();
+ LiveList result = subject.RunningSum().ToLiveList();
+ subject.OnNext(1); subject.OnNext(2); subject.OnNext(3);
+ subject.OnCompleted();
+ Assert.Equal(new[] { 1, 3, 6 }, result.ToArray());
+ }
+
+ [Fact]
+ public void RunningMin_EmitsCurrentMinimum()
+ {
+ Subject subject = new();
+ LiveList result = subject.RunningMin().ToLiveList();
+ subject.OnNext(5); subject.OnNext(3); subject.OnNext(7); subject.OnNext(1);
+ subject.OnCompleted();
+ Assert.Equal(new[] { 5, 3, 3, 1 }, result.ToArray());
+ }
+
+ [Fact]
+ public void RunningMax_EmitsCurrentMaximum()
+ {
+ Subject subject = new();
+ LiveList result = subject.RunningMax().ToLiveList();
+ subject.OnNext(1); subject.OnNext(5); subject.OnNext(3); subject.OnNext(7);
+ subject.OnCompleted();
+ Assert.Equal(new[] { 1, 5, 5, 7 }, result.ToArray());
+ }
+
+ [Fact]
+ public void RunningAverage_EmitsRunningAverageOfDoubles()
+ {
+ Subject subject = new();
+ LiveList result = subject.RunningAverage().ToLiveList();
+ subject.OnNext(2.0); subject.OnNext(4.0); subject.OnNext(6.0);
+ subject.OnCompleted();
+ Assert.Equal(new[] { 2.0, 3.0, 4.0 }, result.ToArray());
+ }
+
+ [Fact]
+ public void RunningAverage_EmitsRunningAverageOfDecimals()
+ {
+ Subject subject = new();
+ LiveList result = subject.RunningAverage().ToLiveList();
+ subject.OnNext(2m); subject.OnNext(4m); subject.OnNext(6m);
+ subject.OnCompleted();
+ Assert.Equal(new[] { 2m, 3m, 4m }, result.ToArray());
+ }
+
+ [Fact]
+ public void RunningAverage_EmitsRunningAverageOfInts()
+ {
+ Subject subject = new();
+ LiveList result = subject.RunningAverage().ToLiveList();
+ subject.OnNext(1); subject.OnNext(3); subject.OnNext(5);
+ subject.OnCompleted();
+ Assert.Equal(new[] { 1.0, 2.0, 3.0 }, result.ToArray());
+ }
+
+ [Fact]
+ public void RunningMin_WithCustomComparer_EmitsCurrentMinimum()
+ {
+ Subject subject = new();
+ LiveList result = subject.RunningMin(StringComparer.Ordinal).ToLiveList();
+ subject.OnNext("banana"); subject.OnNext("apple"); subject.OnNext("cherry");
+ subject.OnCompleted();
+ Assert.Equal(new[] { "banana", "apple", "apple" }, result.ToArray());
+ }
+
+ [Fact]
+ public void RunningMax_WithCustomComparer_EmitsCurrentMaximum()
+ {
+ Subject subject = new();
+ LiveList result = subject.RunningMax(StringComparer.Ordinal).ToLiveList();
+ subject.OnNext("apple"); subject.OnNext("cherry"); subject.OnNext("banana");
+ subject.OnCompleted();
+ Assert.Equal(new[] { "apple", "cherry", "cherry" }, result.ToArray());
+ }
+
+ [Fact]
+ public void RunningCount_EmptySequence_EmitsNothing()
+ {
+ Subject subject = new();
+ LiveList result = subject.RunningCount().ToLiveList();
+ subject.OnCompleted();
+ Assert.Empty(result.ToArray());
+ }
+}
diff --git a/R3Ext.Tests/CacheFilterTests.cs b/R3Ext.Tests/CacheFilterTests.cs
index 6cd3be0..52f2cfe 100644
--- a/R3Ext.Tests/CacheFilterTests.cs
+++ b/R3Ext.Tests/CacheFilterTests.cs
@@ -2,9 +2,9 @@
using System.Collections.Generic;
using System.Linq;
using R3;
-using R3.DynamicData.Cache;
-using R3.DynamicData.Kernel;
-using R3.DynamicData.Operators;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.Kernel;
+using R3Ext.DynamicData.Operators;
#pragma warning disable SA1208
#pragma warning disable SA1516
#pragma warning disable SA1501
diff --git a/R3Ext.Tests/CacheOperatorParityPhase2Tests.cs b/R3Ext.Tests/CacheOperatorParityPhase2Tests.cs
index 41be180..eed8466 100644
--- a/R3Ext.Tests/CacheOperatorParityPhase2Tests.cs
+++ b/R3Ext.Tests/CacheOperatorParityPhase2Tests.cs
@@ -5,10 +5,10 @@
using System.Linq;
using System.Threading.Tasks;
using R3;
-using R3.DynamicData.Cache;
-using R3.DynamicData.Kernel;
-using R3.DynamicData.List;
-using R3.DynamicData.Operators;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.Kernel;
+using R3Ext.DynamicData.List;
+using R3Ext.DynamicData.Operators;
using Xunit;
namespace R3Ext.Tests;
@@ -120,7 +120,7 @@ public async Task AddKey_ProducesKeyedChanges()
var results = new List>();
var emitTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- Observable> keyed = list.Connect().AddKey- (i => i.Id);
+ Observable> keyed = list.Connect().AddKey
- (i => i.Id);
using var sub = keyed.QueryWhenChanged().Subscribe(x =>
{
results.Add(x);
diff --git a/R3Ext.Tests/ChangeKeyCacheTests.cs b/R3Ext.Tests/ChangeKeyCacheTests.cs
index 8604066..508daf0 100644
--- a/R3Ext.Tests/ChangeKeyCacheTests.cs
+++ b/R3Ext.Tests/ChangeKeyCacheTests.cs
@@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using R3;
-using R3.DynamicData.Cache;
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.Kernel;
#pragma warning disable SA1516, SA1503, SA1513, SA1107, SA1502, SA1515
namespace R3Ext.Tests;
diff --git a/R3Ext.Tests/CombinationExtensionsTests.cs b/R3Ext.Tests/CombinationExtensionsTests.cs
new file mode 100644
index 0000000..46d2d9f
--- /dev/null
+++ b/R3Ext.Tests/CombinationExtensionsTests.cs
@@ -0,0 +1,424 @@
+#pragma warning disable SA1107, SA1124, SA1501, SA1503, SA1515, SA1025, SA1520, SA1513, SA1508, SA1516
+using System;
+using System.Collections.Generic;
+using R3;
+using R3.Collections;
+using Xunit;
+
+#pragma warning disable SA1503, SA1513, SA1515, SA1107, SA1502, SA1508, SA1516
+
+namespace R3Ext.Tests;
+
+public class CombinationExtensionsTests
+{
+ #region ForkJoin
+
+ [Fact]
+ public void ForkJoin_EmitsLastValuesWhenAllComplete()
+ {
+ Subject s1 = new(), s2 = new(), s3 = new();
+ LiveList<(int, int, int)> result = CombinationExtensions.ForkJoin(s1, s2, s3).ToLiveList();
+
+ s1.OnNext(1); s1.OnNext(2); s1.OnCompleted();
+ s2.OnNext(10); s2.OnCompleted();
+ Assert.Empty(result.ToArray());
+
+ s3.OnNext(100); s3.OnCompleted();
+ Assert.Single(result.ToArray());
+ Assert.Equal((2, 10, 100), result.ToArray()[0]);
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void ForkJoin_EmptySourcesReturnsEmptyArray()
+ {
+ LiveList result = CombinationExtensions.ForkJoin().ToLiveList();
+ Assert.Single(result.ToArray());
+ Assert.Empty(result.ToArray()[0]);
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void ForkJoin_SingleSourceEmitsLastValue()
+ {
+ Subject s = new();
+ LiveList result = CombinationExtensions.ForkJoin(s).ToLiveList();
+
+ s.OnNext(5);
+ s.OnNext(99);
+ s.OnCompleted();
+
+ Assert.Single(result.ToArray());
+ Assert.Equal(new[] { 99 }, result.ToArray()[0]);
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void ForkJoin_SourceWithNoValueCompletesWithFailure()
+ {
+ Subject s1 = new(), s2 = new();
+ bool completed = false;
+
+ CombinationExtensions.ForkJoin(s1, s2).Subscribe(
+ _ => { },
+ _ => { },
+ r => { completed = true; Assert.True(r.IsFailure); });
+
+ s1.OnNext(1);
+ s1.OnCompleted();
+ s2.OnCompleted(); // s2 never emitted
+
+ Assert.True(completed);
+ }
+
+ [Fact]
+ public void ForkJoin_Typed_TwoSources()
+ {
+ Subject s1 = new();
+ Subject s2 = new();
+ LiveList<(int, string)> result = CombinationExtensions.ForkJoin(s1, s2).ToLiveList();
+
+ s1.OnNext(42); s1.OnCompleted();
+ s2.OnNext("hello"); s2.OnCompleted();
+
+ Assert.Single(result.ToArray());
+ Assert.Equal((42, "hello"), result.ToArray()[0]);
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void ForkJoin_Typed_ThreeSources()
+ {
+ Subject s1 = new();
+ Subject s2 = new();
+ Subject s3 = new();
+ LiveList<(int, string, bool)> result = CombinationExtensions.ForkJoin(s1, s2, s3).ToLiveList();
+
+ s1.OnNext(1); s1.OnCompleted();
+ s2.OnNext("x"); s2.OnCompleted();
+ s3.OnNext(true); s3.OnCompleted();
+
+ Assert.Single(result.ToArray());
+ Assert.Equal((1, "x", true), result.ToArray()[0]);
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void ForkJoin_NullSourcesThrows()
+ {
+ Assert.Throws(() => CombinationExtensions.ForkJoin((IEnumerable>)null!));
+ }
+
+ #endregion
+
+ #region Generate
+
+ [Fact]
+ public void Generate_ProducesSequence()
+ {
+ LiveList result = CombinationExtensions
+ .Generate(0, x => x < 5, x => x + 1, x => x * 2)
+ .ToLiveList();
+
+ Assert.Equal(new[] { 0, 2, 4, 6, 8 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void Generate_EmptyWhenConditionFalseFromStart()
+ {
+ LiveList result = CombinationExtensions
+ .Generate(10, x => x < 5, x => x + 1, x => x)
+ .ToLiveList();
+
+ Assert.Empty(result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void Generate_SingleItem()
+ {
+ LiveList result = CombinationExtensions
+ .Generate(0, x => x < 1, x => x + 1, x => x + 100)
+ .ToLiveList();
+
+ Assert.Equal(new[] { 100 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void Generate_StateOverload_ProducesSequence()
+ {
+ LiveList result = CombinationExtensions
+ .Generate(1, x => x <= 5, x => x + 1)
+ .ToLiveList();
+
+ Assert.Equal(new[] { 1, 2, 3, 4, 5 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void Generate_NullConditionThrows()
+ {
+ Assert.Throws(() =>
+ CombinationExtensions.Generate(0, null!, x => x + 1, x => x));
+ }
+
+ #endregion
+
+ #region Iif / Condition
+
+ [Fact]
+ public void Iif_SelectsBasedOnCondition()
+ {
+ bool flag = true;
+ LiveList result = CombinationExtensions
+ .Iif(() => flag, Observable.Return(1), Observable.Return(2))
+ .ToLiveList();
+ Assert.Equal(new[] { 1 }, result.ToArray());
+
+ flag = false;
+ LiveList result2 = CombinationExtensions
+ .Iif(() => flag, Observable.Return(1), Observable.Return(2))
+ .ToLiveList();
+ Assert.Equal(new[] { 2 }, result2.ToArray());
+ }
+
+ [Fact]
+ public void Iif_EvaluatesConditionAtSubscribeTime()
+ {
+ bool flag = true;
+ Observable obs = CombinationExtensions.Iif(() => flag, Observable.Return(10), Observable.Return(20));
+
+ flag = false;
+ LiveList result = obs.ToLiveList();
+
+ // Condition evaluated at subscribe time (when ToLiveList subscribes), flag is false
+ Assert.Equal(new[] { 20 }, result.ToArray());
+ }
+
+ [Fact]
+ public void Iif_NullConditionThrows()
+ {
+ Assert.Throws(() =>
+ CombinationExtensions.Iif(null!, Observable.Return(1), Observable.Return(2)));
+ }
+
+ [Fact]
+ public void Condition_IsAliasForIif()
+ {
+ bool flag = true;
+ LiveList result = CombinationExtensions
+ .Condition(() => flag, Observable.Return(42), Observable.Return(0))
+ .ToLiveList();
+ Assert.Equal(new[] { 42 }, result.ToArray());
+ }
+
+ #endregion
+
+ #region SequenceEqual
+
+ [Fact]
+ public void SequenceEqual_TrueForEqualSequences()
+ {
+ Subject s1 = new(), s2 = new();
+ LiveList result = s1.SequenceEqual(s2).ToLiveList();
+
+ s1.OnNext(1); s2.OnNext(1);
+ s1.OnNext(2); s2.OnNext(2);
+ s1.OnCompleted(); s2.OnCompleted();
+
+ Assert.Equal(new[] { true }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void SequenceEqual_FalseOnMismatch()
+ {
+ Subject s1 = new(), s2 = new();
+ LiveList result = s1.SequenceEqual(s2).ToLiveList();
+
+ s1.OnNext(1); s2.OnNext(99);
+
+ Assert.Equal(new[] { false }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void SequenceEqual_FalseOnDifferentLengths()
+ {
+ Subject s1 = new(), s2 = new();
+ LiveList result = s1.SequenceEqual(s2).ToLiveList();
+
+ s1.OnNext(1);
+ s1.OnCompleted();
+ s2.OnNext(1);
+ s2.OnNext(2);
+ s2.OnCompleted();
+
+ Assert.Equal(new[] { false }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void SequenceEqual_TrueForBothEmpty()
+ {
+ Subject s1 = new(), s2 = new();
+ LiveList result = s1.SequenceEqual(s2).ToLiveList();
+
+ s1.OnCompleted();
+ s2.OnCompleted();
+
+ Assert.Equal(new[] { true }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void SequenceEqual_UsesCustomComparer()
+ {
+ Subject s1 = new(), s2 = new();
+ LiveList result = s1.SequenceEqual(s2, StringComparer.OrdinalIgnoreCase).ToLiveList();
+
+ s1.OnNext("Hello"); s2.OnNext("hello");
+ s1.OnCompleted(); s2.OnCompleted();
+
+ Assert.Equal(new[] { true }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void SequenceEqual_NullSourceThrows()
+ {
+ Observable nullSource = null!;
+ Assert.Throws(() => nullSource.SequenceEqual(Observable.Return(1)));
+ }
+
+ #endregion
+
+ #region OnErrorResumeNext
+
+ [Fact]
+ public void OnErrorResumeNext_ContinuesAfterError()
+ {
+ Subject s1 = new(), s2 = new();
+ LiveList result = s1.OnErrorResumeNext(s2).ToLiveList();
+
+ s1.OnNext(1);
+ s1.OnErrorResume(new Exception());
+ s2.OnNext(2);
+ s2.OnCompleted();
+
+ Assert.Equal(new[] { 1, 2 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void OnErrorResumeNext_ContinuesAfterSuccess()
+ {
+ Subject s1 = new(), s2 = new();
+ LiveList result = s1.OnErrorResumeNext(s2).ToLiveList();
+
+ s1.OnNext(10);
+ s1.OnCompleted();
+ s2.OnNext(20);
+ s2.OnCompleted();
+
+ Assert.Equal(new[] { 10, 20 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void OnErrorResumeNext_MultipleSources()
+ {
+ Subject s1 = new(), s2 = new(), s3 = new();
+ LiveList result = CombinationExtensions.OnErrorResumeNext(s1, s2, s3).ToLiveList();
+
+ s1.OnNext(1); s1.OnCompleted();
+ s2.OnNext(2); s2.OnErrorResume(new Exception());
+ s3.OnNext(3); s3.OnCompleted();
+
+ Assert.Equal(new[] { 1, 2, 3 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void OnErrorResumeNext_EmptySourcesCompletes()
+ {
+ LiveList result = CombinationExtensions.OnErrorResumeNext().ToLiveList();
+ Assert.Empty(result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void OnErrorResumeNext_NullSourceThrows()
+ {
+ Observable nullSource = null!;
+ Assert.Throws(() => nullSource.OnErrorResumeNext(Observable.Return(1)));
+ }
+
+ #endregion
+
+ #region RepeatWhen
+
+ [Fact]
+ public void RepeatWhen_RepeatsWhenNotifierEmits()
+ {
+ int subscribeCount = 0;
+ Subject trigger = new();
+
+ Observable source = Observable.Create(observer =>
+ {
+ subscribeCount++;
+ observer.OnNext(subscribeCount);
+ observer.OnCompleted();
+ return Disposable.Empty;
+ });
+
+ LiveList result = source.RepeatWhen(_ => trigger).ToLiveList();
+
+ Assert.Equal(new[] { 1 }, result.ToArray());
+ trigger.OnNext(Unit.Default);
+ Assert.Equal(new[] { 1, 2 }, result.ToArray());
+ trigger.OnNext(Unit.Default);
+ Assert.Equal(new[] { 1, 2, 3 }, result.ToArray());
+ }
+
+ [Fact]
+ public void RepeatWhen_StopsWhenHandlerCompletes()
+ {
+ Subject trigger = new();
+ int subscribeCount = 0;
+
+ Observable source = Observable.Create(observer =>
+ {
+ subscribeCount++;
+ observer.OnNext(subscribeCount);
+ observer.OnCompleted();
+ return Disposable.Empty;
+ });
+
+ LiveList result = source.RepeatWhen(_ => trigger).ToLiveList();
+
+ trigger.OnNext(Unit.Default);
+ trigger.OnCompleted();
+
+ Assert.True(result.IsCompleted);
+ Assert.Equal(new[] { 1, 2 }, result.ToArray());
+ }
+
+ [Fact]
+ public void RepeatWhen_NullSourceThrows()
+ {
+ Observable nullSource = null!;
+ Assert.Throws(() => nullSource.RepeatWhen(_ => Observable.Return(Unit.Default)));
+ }
+
+ [Fact]
+ public void RepeatWhen_NullHandlerThrows()
+ {
+ Assert.Throws(() =>
+ Observable.Return(1).RepeatWhen(null!));
+ }
+
+ #endregion
+}
diff --git a/R3Ext.Tests/DynamicDataSortTests.cs b/R3Ext.Tests/DynamicDataSortTests.cs
index 742b766..c837e2d 100644
--- a/R3Ext.Tests/DynamicDataSortTests.cs
+++ b/R3Ext.Tests/DynamicDataSortTests.cs
@@ -3,8 +3,8 @@
using System.ComponentModel;
using System.Linq;
using R3;
-using R3.DynamicData.Binding;
-using R3.DynamicData.List;
+using R3Ext.DynamicData.Binding;
+using R3Ext.DynamicData.List;
using Xunit;
namespace R3Ext.Tests;
diff --git a/R3Ext.Tests/EnsureUniqueKeysCacheTests.cs b/R3Ext.Tests/EnsureUniqueKeysCacheTests.cs
index 1d1ca26..12da8ce 100644
--- a/R3Ext.Tests/EnsureUniqueKeysCacheTests.cs
+++ b/R3Ext.Tests/EnsureUniqueKeysCacheTests.cs
@@ -2,8 +2,8 @@
#pragma warning disable SA1516, SA1515, SA1503, SA1502, SA1107
using System.Linq;
using R3;
-using R3.DynamicData.Cache;
-using R3.DynamicData.Kernel;
+using R3Ext.DynamicData.Cache;
+using R3Ext.DynamicData.Kernel;
namespace R3Ext.Tests;
diff --git a/R3Ext.Tests/ErrorHandlingExtraTests.cs b/R3Ext.Tests/ErrorHandlingExtraTests.cs
new file mode 100644
index 0000000..228b182
--- /dev/null
+++ b/R3Ext.Tests/ErrorHandlingExtraTests.cs
@@ -0,0 +1,161 @@
+using R3;
+using R3.Collections;
+
+#pragma warning disable SA1503, SA1513, SA1515, SA1107, SA1502, SA1508, SA1516
+
+namespace R3Ext.Tests;
+
+public class ErrorHandlingExtraTests
+{
+ [Fact]
+ public void ReplaceError_EmitsFallbackOnErrorResume()
+ {
+ Subject subject = new();
+ LiveList result = subject.ReplaceError(99).ToLiveList();
+ subject.OnNext(1);
+ subject.OnErrorResume(new Exception("boom"));
+ Assert.Equal(new[] { 1, 99 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void ReplaceError_NullSource_ThrowsArgumentNullException()
+ {
+ Observable nullSource = null!;
+ Assert.Throws(() => nullSource.ReplaceError(0));
+ }
+
+ [Fact]
+ public void ReplaceError_ForwardsSuccessCompletion()
+ {
+ Subject subject = new();
+ LiveList result = subject.ReplaceError(99).ToLiveList();
+ subject.OnNext(1);
+ subject.OnCompleted();
+ Assert.Equal(new[] { 1 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void ReplaceEmpty_EmitsFallbackWhenNoValues()
+ {
+ Subject subject = new();
+ LiveList result = subject.ReplaceEmpty(42).ToLiveList();
+ subject.OnCompleted();
+ Assert.Equal(new[] { 42 }, result.ToArray());
+ }
+
+ [Fact]
+ public void ReplaceEmpty_DoesNotEmitFallbackWhenHasValues()
+ {
+ Subject subject = new();
+ LiveList result = subject.ReplaceEmpty(42).ToLiveList();
+ subject.OnNext(1);
+ subject.OnCompleted();
+ Assert.Equal(new[] { 1 }, result.ToArray());
+ }
+
+ [Fact]
+ public void ReplaceEmpty_NullSource_ThrowsArgumentNullException()
+ {
+ Observable nullSource = null!;
+ Assert.Throws(() => nullSource.ReplaceEmpty(0));
+ }
+
+ [Fact]
+ public void SelectSafe_RoutesExceptionToOnErrorResume()
+ {
+ Subject subject = new();
+ List errors = new();
+ LiveList result = subject.SelectSafe(x =>
+ {
+ if (x == 0) throw new DivideByZeroException();
+ return x.ToString();
+ }).DoOnError(ex => errors.Add(ex)).ToLiveList();
+
+ subject.OnNext(1);
+ subject.OnNext(0);
+ subject.OnNext(2);
+ subject.OnCompleted();
+ Assert.Equal(new[] { "1", "2" }, result.ToArray());
+ Assert.Single(errors);
+ Assert.IsType(errors[0]);
+ }
+
+ [Fact]
+ public void SelectSafe_NullSource_ThrowsArgumentNullException()
+ {
+ Observable nullSource = null!;
+ Assert.Throws(() => nullSource.SelectSafe(x => x.ToString()));
+ }
+
+ [Fact]
+ public void WhereSafe_RoutesExceptionToOnErrorResume()
+ {
+ Subject subject = new();
+ List errors = new();
+ LiveList result = subject.WhereSafe(x =>
+ {
+ if (x < 0) throw new ArgumentOutOfRangeException();
+ return x % 2 == 0;
+ }).DoOnError(ex => errors.Add(ex)).ToLiveList();
+
+ subject.OnNext(2);
+ subject.OnNext(-1);
+ subject.OnNext(4);
+ subject.OnCompleted();
+ Assert.Equal(new[] { 2, 4 }, result.ToArray());
+ Assert.Single(errors);
+ Assert.IsType(errors[0]);
+ }
+
+ [Fact]
+ public void RetryWhen_RetriesOnHandlerSignal()
+ {
+ int attempts = 0;
+ // Use Observable.Create so events fire after subscription (not before like with pre-signaled Subject)
+ Observable src = Observable.Create(observer =>
+ {
+ attempts++;
+ if (attempts < 3)
+ {
+ observer.OnErrorResume(new Exception("fail"));
+ }
+ else
+ {
+ observer.OnNext(42);
+ observer.OnCompleted();
+ }
+ return Disposable.Empty;
+ });
+
+ LiveList result = src.RetryWhen(errors => errors.Select(_ => Unit.Default)).ToLiveList();
+ Assert.Equal(new[] { 42 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void RetryWhen_CompletesWhenHandlerCompletes()
+ {
+ Subject subject = new();
+ Subject trigger = new();
+ LiveList result = subject.RetryWhen(_ => trigger).ToLiveList();
+ subject.OnNext(1);
+ trigger.OnCompleted();
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void RetryWhen_NullSource_ThrowsArgumentNullException()
+ {
+ Observable nullSource = null!;
+ Assert.Throws(() => nullSource.RetryWhen(_ => Observable.Empty()));
+ }
+
+ [Fact]
+ public void RetryWhen_NullHandler_ThrowsArgumentNullException()
+ {
+ Subject subject = new();
+ Assert.Throws(() => subject.RetryWhen(null!));
+ }
+}
diff --git a/R3Ext.Tests/FilteringAdvancedTests.cs b/R3Ext.Tests/FilteringAdvancedTests.cs
new file mode 100644
index 0000000..647ab4b
--- /dev/null
+++ b/R3Ext.Tests/FilteringAdvancedTests.cs
@@ -0,0 +1,519 @@
+using Microsoft.Extensions.Time.Testing;
+using R3;
+using R3.Collections;
+
+namespace R3Ext.Tests;
+
+public class FilteringAdvancedTests
+{
+ // โโโ IgnoreElements โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ [Fact]
+ public void IgnoreElements_SuppressesOnNext()
+ {
+ Subject subject = new();
+ LiveList result = subject.IgnoreElements().ToLiveList();
+ subject.OnNext(1);
+ subject.OnNext(2);
+ subject.OnNext(3);
+ Assert.Empty(result.ToArray());
+ subject.OnCompleted();
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void IgnoreElements_ForwardsError()
+ {
+ Subject subject = new();
+ List errors = new();
+ using IDisposable _ = subject.IgnoreElements().Subscribe(_ => { }, errors.Add, _ => { });
+ subject.OnErrorResume(new InvalidOperationException("boom"));
+ Assert.Single(errors);
+ Assert.IsType(errors[0]);
+ }
+
+ [Fact]
+ public void IgnoreElements_ForwardsCompletion()
+ {
+ Subject subject = new();
+ LiveList result = subject.IgnoreElements().ToLiveList();
+ subject.OnNext(42);
+ Assert.Empty(result.ToArray());
+ subject.OnCompleted();
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void IgnoreElements_ThrowsOnNullSource()
+ {
+ Observable? nullSource = null;
+ Assert.Throws(() => nullSource!.IgnoreElements());
+ }
+
+ // โโโ IsEmpty โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ [Fact]
+ public void IsEmpty_TrueWhenNoElements()
+ {
+ Subject subject = new();
+ LiveList result = subject.IsEmpty().ToLiveList();
+ subject.OnCompleted();
+ Assert.Equal(new[] { true }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void IsEmpty_FalseOnFirstElement()
+ {
+ Subject subject = new();
+ LiveList result = subject.IsEmpty().ToLiveList();
+ subject.OnNext(42);
+ Assert.Equal(new[] { false }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void IsEmpty_CompletesAfterFirstElement_IgnoresSubsequent()
+ {
+ Subject subject = new();
+ LiveList result = subject.IsEmpty().ToLiveList();
+ subject.OnNext(1);
+ subject.OnNext(2);
+ subject.OnNext(3);
+ Assert.Equal(new[] { false }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void IsEmpty_ThrowsOnNullSource()
+ {
+ Observable? nullSource = null;
+ Assert.Throws(() => nullSource!.IsEmpty());
+ }
+
+ // โโโ Every / All โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ [Fact]
+ public void Every_FalseOnFirstFail()
+ {
+ Subject subject = new();
+ LiveList result = subject.Every(x => x > 0).ToLiveList();
+ subject.OnNext(1);
+ subject.OnNext(-1);
+ Assert.Equal(new[] { false }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void Every_TrueOnCompletion()
+ {
+ Subject subject = new();
+ LiveList result = subject.Every(x => x > 0).ToLiveList();
+ subject.OnNext(1);
+ subject.OnNext(2);
+ subject.OnCompleted();
+ Assert.Equal(new[] { true }, result.ToArray());
+ }
+
+ [Fact]
+ public void Every_TrueWhenEmpty()
+ {
+ Subject subject = new();
+ LiveList result = subject.Every(x => x > 0).ToLiveList();
+ subject.OnCompleted();
+ Assert.Equal(new[] { true }, result.ToArray());
+ }
+
+ [Fact]
+ public void Every_FalseOnVeryFirstValue()
+ {
+ Subject subject = new();
+ LiveList result = subject.Every(x => x > 0).ToLiveList();
+ subject.OnNext(-5);
+ Assert.Equal(new[] { false }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void All_IsAliasForEvery()
+ {
+ Subject subject = new();
+ LiveList result = subject.All(x => x > 0).ToLiveList();
+ subject.OnNext(1);
+ subject.OnNext(2);
+ subject.OnCompleted();
+ Assert.Equal(new[] { true }, result.ToArray());
+ }
+
+ [Fact]
+ public void Every_ThrowsOnNullSource()
+ {
+ Observable? nullSource = null;
+ Assert.Throws(() => nullSource!.Every(x => x > 0));
+ }
+
+ [Fact]
+ public void Every_ThrowsOnNullPredicate()
+ {
+ Subject subject = new();
+ Assert.Throws(() => subject.Every(null!));
+ }
+
+ // โโโ Find โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ [Fact]
+ public void Find_EmitsFirstMatchAndCompletes()
+ {
+ Subject subject = new();
+ LiveList result = subject.Find(x => x > 5).ToLiveList();
+ subject.OnNext(1);
+ subject.OnNext(2);
+ subject.OnNext(10);
+ subject.OnNext(20);
+ Assert.Equal(new[] { 10 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void Find_CompletesWithoutEmitWhenNoMatch()
+ {
+ Subject subject = new();
+ LiveList result = subject.Find(x => x > 100).ToLiveList();
+ subject.OnNext(1);
+ subject.OnNext(2);
+ subject.OnCompleted();
+ Assert.Empty(result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void Find_EmitsFirstMatchOnly()
+ {
+ Subject subject = new();
+ LiveList result = subject.Find(x => x % 2 == 0).ToLiveList();
+ subject.OnNext(1);
+ subject.OnNext(2);
+ subject.OnNext(4);
+ Assert.Equal(new[] { 2 }, result.ToArray());
+ }
+
+ [Fact]
+ public void Find_ThrowsOnNullSource()
+ {
+ Observable? nullSource = null;
+ Assert.Throws(() => nullSource!.Find(x => x > 0));
+ }
+
+ // โโโ FindIndex โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ [Fact]
+ public void FindIndex_EmitsCorrectIndex()
+ {
+ Subject subject = new();
+ LiveList result = subject.FindIndex(x => x > 5).ToLiveList();
+ subject.OnNext(1);
+ subject.OnNext(2);
+ subject.OnNext(10);
+ Assert.Equal(new[] { 2 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void FindIndex_EmitsZeroForFirstElement()
+ {
+ Subject subject = new();
+ LiveList result = subject.FindIndex(x => x > 0).ToLiveList();
+ subject.OnNext(99);
+ Assert.Equal(new[] { 0 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void FindIndex_CompletesWithoutEmitWhenNoMatch()
+ {
+ Subject subject = new();
+ LiveList result = subject.FindIndex(x => x > 100).ToLiveList();
+ subject.OnNext(1);
+ subject.OnNext(2);
+ subject.OnCompleted();
+ Assert.Empty(result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void FindIndex_ThrowsOnNullSource()
+ {
+ Observable? nullSource = null;
+ Assert.Throws(() => nullSource!.FindIndex(x => x > 0));
+ }
+
+ // โโโ DefaultIfEmpty โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ [Fact]
+ public void DefaultIfEmpty_EmitsDefaultWhenEmpty()
+ {
+ Subject subject = new();
+ LiveList result = subject.DefaultIfEmpty(99).ToLiveList();
+ subject.OnCompleted();
+ Assert.Equal(new[] { 99 }, result.ToArray());
+ }
+
+ [Fact]
+ public void DefaultIfEmpty_DoesNotEmitDefaultWhenHasValues()
+ {
+ Subject subject = new();
+ LiveList result = subject.DefaultIfEmpty(99).ToLiveList();
+ subject.OnNext(1);
+ subject.OnCompleted();
+ Assert.Equal(new[] { 1 }, result.ToArray());
+ }
+
+ [Fact]
+ public void DefaultIfEmpty_ForwardsAllValuesWhenPresent()
+ {
+ Subject subject = new();
+ LiveList result = subject.DefaultIfEmpty(0).ToLiveList();
+ subject.OnNext(1);
+ subject.OnNext(2);
+ subject.OnNext(3);
+ subject.OnCompleted();
+ Assert.Equal(new[] { 1, 2, 3 }, result.ToArray());
+ }
+
+ [Fact]
+ public void DefaultIfEmpty_ThrowsOnNullSource()
+ {
+ Observable? nullSource = null;
+ Assert.Throws(() => nullSource!.DefaultIfEmpty(0));
+ }
+
+ // โโโ ThrowIfEmpty โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ [Fact]
+ public void ThrowIfEmpty_ThrowsDefaultExceptionWhenEmpty()
+ {
+ Subject subject = new();
+ List completions = new();
+ using IDisposable _ = subject.ThrowIfEmpty().Subscribe(_ => { }, _ => { }, completions.Add);
+ subject.OnCompleted();
+ Assert.Single(completions);
+ Assert.True(completions[0].IsFailure);
+ Assert.IsType(completions[0].Exception);
+ }
+
+ [Fact]
+ public void ThrowIfEmpty_UsesCustomExceptionFactory()
+ {
+ Subject subject = new();
+ List completions = new();
+ using IDisposable _ = subject
+ .ThrowIfEmpty(() => new ArgumentException("custom"))
+ .Subscribe(_ => { }, _ => { }, completions.Add);
+ subject.OnCompleted();
+ Assert.Single(completions);
+ Assert.True(completions[0].IsFailure);
+ Assert.IsType(completions[0].Exception);
+ }
+
+ [Fact]
+ public void ThrowIfEmpty_DoesNotThrowWhenHasValues()
+ {
+ Subject subject = new();
+ LiveList result = subject.ThrowIfEmpty().ToLiveList();
+ subject.OnNext(1);
+ subject.OnCompleted();
+ Assert.Equal(new[] { 1 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void ThrowIfEmpty_ThrowsOnNullSource()
+ {
+ Observable? nullSource = null;
+ Assert.Throws(() => nullSource!.ThrowIfEmpty());
+ }
+
+ // โโโ Audit โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ [Fact]
+ public void Audit_EmitsLatestWhenDurationFires()
+ {
+ Subject source = new();
+ Subject durationSubject = new();
+ LiveList result = source.Audit(_ => durationSubject).ToLiveList();
+
+ source.OnNext(1);
+ source.OnNext(2);
+ durationSubject.OnNext(Unit.Default);
+
+ Assert.Equal(new[] { 2 }, result.ToArray());
+ }
+
+ [Fact]
+ public void Audit_EmitsNothingIfNoDurationFire()
+ {
+ Subject source = new();
+ Subject duration = new();
+ LiveList result = source.Audit(_ => duration).ToLiveList();
+
+ source.OnNext(1);
+ source.OnNext(2);
+ source.OnCompleted();
+
+ Assert.Empty(result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void Audit_ResetsAfterDurationFiresAndAcceptsNewValue()
+ {
+ Subject source = new();
+ Subject duration1 = new();
+ Subject duration2 = new();
+ int call = 0;
+ Subject[] durations = [duration1, duration2];
+
+ LiveList result = source.Audit(_ => durations[call++ % 2]).ToLiveList();
+
+ source.OnNext(10);
+ duration1.OnNext(Unit.Default);
+ Assert.Equal(new[] { 10 }, result.ToArray());
+
+ source.OnNext(20);
+ duration2.OnNext(Unit.Default);
+ Assert.Equal(new[] { 10, 20 }, result.ToArray());
+ }
+
+ [Fact]
+ public void Audit_ThrowsOnNullSource()
+ {
+ Observable? nullSource = null;
+ Assert.Throws(() => nullSource!.Audit(_ => Observable.Never()));
+ }
+
+ // โโโ AuditTime โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ [Fact]
+ public void AuditTime_EmitsLatestAfterDuration()
+ {
+ FakeTimeProvider tp = new();
+ Subject source = new();
+ LiveList result = source.AuditTime(TimeSpan.FromSeconds(1), tp).ToLiveList();
+
+ source.OnNext(1);
+ source.OnNext(2);
+ tp.Advance(TimeSpan.FromSeconds(1));
+
+ Assert.Equal(new[] { 2 }, result.ToArray());
+ }
+
+ [Fact]
+ public void AuditTime_DoesNotEmitBeforeDuration()
+ {
+ FakeTimeProvider tp = new();
+ Subject source = new();
+ LiveList result = source.AuditTime(TimeSpan.FromSeconds(5), tp).ToLiveList();
+
+ source.OnNext(42);
+ tp.Advance(TimeSpan.FromSeconds(4));
+
+ Assert.Empty(result.ToArray());
+ }
+
+ [Fact]
+ public void AuditTime_CompletesWhenSourceCompletes()
+ {
+ FakeTimeProvider tp = new();
+ Subject source = new();
+ LiveList result = source.AuditTime(TimeSpan.FromSeconds(1), tp).ToLiveList();
+
+ source.OnCompleted();
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void AuditTime_ThrowsOnNullSource()
+ {
+ Observable? nullSource = null;
+ Assert.Throws(() => nullSource!.AuditTime(TimeSpan.FromSeconds(1)));
+ }
+
+ // โโโ Sample โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ [Fact]
+ public void Sample_EmitsLatestWhenSamplerFires()
+ {
+ Subject source = new();
+ Subject sampler = new();
+ LiveList result = source.Sample(sampler).ToLiveList();
+
+ source.OnNext(1);
+ source.OnNext(2);
+ sampler.OnNext(Unit.Default);
+
+ Assert.Equal(new[] { 2 }, result.ToArray());
+ }
+
+ [Fact]
+ public void Sample_EmitsNothingIfNoSourceValue()
+ {
+ Subject source = new();
+ Subject sampler = new();
+ LiveList result = source.Sample(sampler).ToLiveList();
+
+ sampler.OnNext(Unit.Default);
+ sampler.OnNext(Unit.Default);
+
+ Assert.Empty(result.ToArray());
+ }
+
+ [Fact]
+ public void Sample_ResetsAfterEachSample()
+ {
+ Subject source = new();
+ Subject sampler = new();
+ LiveList result = source.Sample(sampler).ToLiveList();
+
+ source.OnNext(10);
+ sampler.OnNext(Unit.Default);
+ Assert.Equal(new[] { 10 }, result.ToArray());
+
+ sampler.OnNext(Unit.Default); // no new value since last sample
+ Assert.Equal(new[] { 10 }, result.ToArray()); // nothing new emitted
+
+ source.OnNext(20);
+ sampler.OnNext(Unit.Default);
+ Assert.Equal(new[] { 10, 20 }, result.ToArray());
+ }
+
+ [Fact]
+ public void Sample_WithTypedSampler_EmitsLatest()
+ {
+ Subject source = new();
+ Subject sampler = new();
+ LiveList result = source.Sample(sampler).ToLiveList();
+
+ source.OnNext(5);
+ sampler.OnNext("tick");
+
+ Assert.Equal(new[] { 5 }, result.ToArray());
+ }
+
+ [Fact]
+ public void Sample_CompletesWhenSourceCompletes()
+ {
+ Subject source = new();
+ Subject sampler = new();
+ LiveList result = source.Sample(sampler).ToLiveList();
+
+ source.OnCompleted();
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void Sample_ThrowsOnNullSource()
+ {
+ Observable? nullSource = null;
+ Assert.Throws(() => nullSource!.Sample(new Subject()));
+ }
+}
diff --git a/R3Ext.Tests/FlatMapExtensionsTests.cs b/R3Ext.Tests/FlatMapExtensionsTests.cs
new file mode 100644
index 0000000..7fc5d47
--- /dev/null
+++ b/R3Ext.Tests/FlatMapExtensionsTests.cs
@@ -0,0 +1,430 @@
+using R3;
+using R3.Collections;
+
+namespace R3Ext.Tests;
+
+public class FlatMapExtensionsTests
+{
+ // โโ ConcatMap โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ [Fact]
+ public void ConcatMap_ProcessesSequentially()
+ {
+ Subject subject = new();
+ Subject inner1 = new();
+ Subject inner2 = new();
+
+ LiveList result = subject.ConcatMap(x => x == 1 ? inner1 : inner2).ToLiveList();
+
+ subject.OnNext(1);
+ subject.OnNext(2); // queued; inner1 not yet done
+
+ inner1.OnNext("a");
+ Assert.Equal(new[] { "a" }, result.ToArray()); // inner2 not started yet
+
+ inner1.OnCompleted();
+ inner2.OnNext("b");
+ Assert.Equal(new[] { "a", "b" }, result.ToArray());
+ }
+
+ [Fact]
+ public void ConcatMap_EmitsInOrder()
+ {
+ Subject subject = new();
+ Subject inner1 = new();
+ Subject inner2 = new();
+
+ LiveList result = subject.ConcatMap(x => x == 1 ? inner1 : inner2).ToLiveList();
+
+ subject.OnNext(1);
+ subject.OnNext(2);
+ inner1.OnNext(10);
+ inner1.OnNext(11);
+ inner1.OnCompleted();
+ inner2.OnNext(20);
+ inner2.OnCompleted();
+ subject.OnCompleted();
+
+ Assert.Equal(new[] { 10, 11, 20 }, result.ToArray());
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void ConcatMap_CompletionPropagatesAfterAllInners()
+ {
+ Subject subject = new();
+ Subject inner = new();
+
+ LiveList result = subject.ConcatMap(_ => inner).ToLiveList();
+
+ subject.OnNext(1);
+ subject.OnCompleted();
+ Assert.False(result.IsCompleted); // inner still active
+
+ inner.OnNext("a");
+ inner.OnCompleted();
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void ConcatMap_CompletesImmediatelyWhenNoInners()
+ {
+ Subject subject = new();
+ LiveList result = subject.ConcatMap(x => Observable.Return(x)).ToLiveList();
+
+ subject.OnCompleted();
+
+ Assert.True(result.IsCompleted);
+ }
+
+ [Fact]
+ public void ConcatMap_ThrowsOnNullSource()
+ {
+ Observable? source = null;
+ Assert.Throws(() => source!.ConcatMap(x => Observable.Return(x)));
+ }
+
+ [Fact]
+ public void ConcatMap_ThrowsOnNullSelector()
+ {
+ Subject subject = new();
+ Assert.Throws(() => subject.ConcatMap(null!));
+ }
+
+ // โโ SwitchMap โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ [Fact]
+ public void SwitchMap_CancelsOnNewValue()
+ {
+ Subject