Skip to content

ci: add merge-gate orchestrator to harden against dropped pull_request webhooks#865

Merged
danielmeppiel merged 5 commits intomainfrom
ci/merge-gate-and-watchdog
Apr 23, 2026
Merged

ci: add merge-gate orchestrator to harden against dropped pull_request webhooks#865
danielmeppiel merged 5 commits intomainfrom
ci/merge-gate-and-watchdog

Conversation

@danielmeppiel
Copy link
Copy Markdown
Collaborator

@danielmeppiel danielmeppiel commented Apr 23, 2026

Why

PR #856 sat with Build & Test (Linux) stuck at Expected -- Waiting for status to be reported despite the same commit having a green run on main minutes earlier. Root cause: GitHub silently dropped the pull_request webhook delivery for that PR. There was no retrigger mechanism, no failure annotation, nothing actionable in the UI. The PR sat in limbo until I noticed.

This is a structural fragility, not a one-off bug. Required-status-checks in branch protection are name-based, not workflow-based. If the workflow that produces a given check name never dispatches, the check stays in Expected forever. There is no built-in timeout.

What this PR does

Adds one new workflow (Merge Gate) that turns the failure mode into a clear red check.

                 +---------------------------+
   PR push ----> | pull_request webhook      |  drops sometimes (~rare,
                 |                           |   non-deterministic)
                 +-------------+-------------+
                               |
                               v
                 +---------------------------+
                 | ci.yml (Build & Test ...) |
                 |                           |
                 +---------------------------+
                               |
                               v
                 +---------------------------+
                 | Required status check     |
                 | "Build & Test (Linux)"    |  <-- stuck "Expected"
                 +---------------------------+   if webhook dropped

After this PR:

                 +---------------------------+
   PR push ----> | pull_request webhook      |---+
                 +---------------------------+   |
                                                 v
                 +---------------------------+   +---------------------------+
                 | ci.yml (Build & Test ...) |   | merge-gate.yml ("gate")   |
                 +-----------+---------------+   |   triggers on BOTH        |
                             |                   |   pull_request AND        |
                             v                   |   pull_request_target     |
                 +---------------------------+   +-----------+---------------+
                 | check "Build & Test       |               |
                 | (Linux)" (when it runs)   |               v
                 +---------------------------+   +---------------------------+
                                                 | polls Checks API for      |
                                                 | "Build & Test (Linux)"    |
                                                 | up to 30 min              |
                                                 |   never appears -> red X  |
                                                 |   appeared, succeeded -> 0|
                                                 |   appeared, failed -> 1   |
                                                 +---------------------------+

The gate triggers on both pull_request and pull_request_target (with concurrency dedup). If pull_request drops, pull_request_target still fires the gate. Single dropped webhook no longer creates a stuck PR.

Files

File Lines Role
.github/workflows/merge-gate.yml ~95 Orchestrator: polls Checks API for Build & Test (Linux), fails red if it never appears
.github/scripts/ci/merge_gate_wait.sh ~110 Polling logic, distinct exit codes (0 ok, 1 failed, 2 never appeared, 3 stuck mid-run, 4 invalid env)
CHANGELOG.md +1 Unreleased entry

Security review

The orchestrator runs on two events with different security postures:

Event Token scope Secrets Checkout PR code? Why safe
pull_request read-only none yes (sparse, scripts dir only) standard PR runner; no privilege
pull_request_target read-write available no fetches script from base ref via API; never executes PR-controlled code

pull_request_target is the historically-dangerous trigger. We use it only to fetch the trusted base-ref script via the GitHub API and execute it; we never actions/checkout the PR head on this event. This matches the same pattern used by ci-integration-pr-stub.yml already in the repo.

Self-bootstrap

Chicken-and-egg: this PR adds the script the gate depends on. On the very first run (this PR), the script doesn't exist on main yet, so the API fetch on pull_request_target returns 404. We handle this with a self-bootstrap fallback: write a no-op exit 0 and emit ::warning:: so the PR can land. Once merged, all future PRs hit the real script.

Test results (live)

Test Command Result
shellcheck shellcheck .github/scripts/ci/*.sh clean
Wait script success path run against PR #856 head SHA exit 0, "tier 1 finished: success"
Wait script timeout path run against 0000...0000 SHA exit 2, clear ::error annotation
Gate dogfood (this PR) watch gate check on this PR pass 1m34s alongside Build & Test (Linux) 1m19s
curl bootstrap retry --retry 5 --retry-delay 3 --retry-connrefused --max-time 30 tolerates transient 5xx

Phase 2: flip plan (post-merge, manual)

After this PR merges, branch protection needs to be updated to make Merge Gate / gate required. I will execute via API once you ack the merge:

# Read current required checks
gh api repos/microsoft/apm/branches/main/protection/required_status_checks

# Add "Merge Gate / gate" to the list, keep "Build & Test (Linux)" as backstop
gh api -X PUT repos/microsoft/apm/branches/main/protection/required_status_checks \
  -f 'strict=true' \
  -F 'contexts[]=Build & Test (Linux)' \
  -F 'contexts[]=Merge Gate / gate' \
  ...

After ~1 week of clean operation, drop Build & Test (Linux) from required and let Merge Gate / gate be the sole required check. The 4 stub names from ci-integration-pr-stub.yml (Build/Smoke/Integration/Release-Validate Linux) can also be cleaned up once the gate covers the same surface.

Rollback

  • Revert this PR.
  • Or: leave files in place, just don't add Merge Gate / gate to required checks. Workflow becomes a no-op informational check.

Out of scope (deliberate)

  • Watchdog cron: I initially shipped a watchdog (cron every 15min, posts recovery comments on stuck PRs) as a transition-period safety net. Dropped it: since we flip to required immediately after merge, the gate's dual-trigger redundancy is sufficient. The watchdog only added value for the double-drop case (both pull_request AND pull_request_target fail simultaneously), which is vanishingly rare. We can add it back if we ever observe one.
  • Replacing ci.yml: this PR adds a gate, not a rewrite. ci.yml keeps doing its job.
  • Merge queue tuning: orthogonal; the gate works regardless of whether the PR uses merge queue or direct push.

PR #856 surfaced a structural CI fragility: required status checks are
satisfied by two independent webhook channels (pull_request emits
'Build & Test (Linux)', pull_request_target emits the four Tier 2
stubs). When the pull_request delivery is dropped, 4/5 stubs go green
and the 5th hangs in 'Expected -- Waiting' forever -- ambiguous yellow
indistinguishable from a slow build. Recovery is folklore.

This PR ships two safety nets in shadow mode:

* .github/workflows/merge-gate.yml + scripts/ci/merge_gate_wait.sh
  Single orchestrator that polls the Checks API for 'Build & Test
  (Linux)' on the PR head SHA and aggregates into one verdict. Triggers
  on BOTH pull_request and pull_request_target so a single dropped
  delivery is recoverable; concurrency dedupes. Times out cleanly with
  a clear error message if Tier 1 never dispatched -- turning the
  invisible failure into a loud red check. NOT YET REQUIRED -- shadow
  observation first, ruleset flip after merge.

* .github/workflows/watchdog-stuck-prs.yml + scripts/ci/watchdog_scan.sh
  Cron every 15 min. For open non-draft PRs with no ci.yml run on the
  head SHA AND non-paths-ignored files, posts one recovery comment.
  Backstop for any required check that stops dispatching.

Tested live (dry-run) against microsoft/apm: watchdog correctly
distinguishes stuck PRs (#853, #409) from docs-only PRs (#864, #461,
#825). Both scripts shellcheck-clean. merge_gate_wait.sh tested
end-to-end against PR #856 head SHA (success path, exit 0) and a
non-existent SHA (timeout path, exit 2 with clear error annotation).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 23, 2026 07:50
Two fixes for the script-fetch step:

1. On 'pull_request' the runner has no secrets and a read-only token, so
   actions/checkout of PR head is safe -- use it for simplicity. We only
   need API-fetch under 'pull_request_target' where checkout would be a
   security risk.
2. On 'pull_request_target', when the script does not yet exist on base
   (i.e. THIS PR is the one adding it), curl returns 404 and we degrade
   to a self-bootstrap no-op pass instead of failing. Once the script
   lands on main, the gate becomes real.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds CI-side safety nets to detect and surface the rare failure mode where GitHub drops pull_request webhook deliveries, leaving required checks stuck in an ambiguous "Expected -- Waiting" state.

Changes:

  • Introduces a Merge Gate orchestrator workflow that polls the Checks API for the Tier 1 PR check and fails with a clear error if the check never appears.
  • Adds a scheduled watchdog workflow + script to scan open PRs and post a one-time recovery comment when the Tier 1 workflow never dispatched.
  • Adds a changelog entry describing the new CI safety nets.
Show a summary per file
File Description
CHANGELOG.md Adds an Unreleased entry describing the merge gate + watchdog rollout.
.github/workflows/merge-gate.yml New orchestrator workflow that waits for Tier 1 and turns "missing check" into a failing gate.
.github/scripts/ci/merge_gate_wait.sh New polling script for check-runs on a commit SHA with distinct exit codes for failure modes.
.github/workflows/watchdog-stuck-prs.yml New scheduled workflow that runs the PR scan every 15 minutes (and via manual dispatch).
.github/scripts/ci/watchdog_scan.sh New scanning + idempotent comment-posting logic to help contributors recover from stuck CI dispatch.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 5

Comment thread .github/scripts/ci/watchdog_scan.sh Outdated
Comment on lines +106 to +113
non_ignored=$(gh api \
-H "Accept: application/vnd.github+json" \
"repos/${REPO}/pulls/${number}/files?per_page=300" \
--jq '[.[].filename] | map(select(
(startswith("docs/") | not)
and . != ".gitignore"
and . != "LICENSE"
)) | length' 2>/dev/null || echo "0")
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changed-files query only fetches the first page (per_page=300) and doesn't paginate. For PRs touching >300 files, non_ignored can be computed from an incomplete file list and misclassify a non-docs change as docs-only, skipping a truly stuck PR. Consider using gh api --paginate (or GraphQL) to ensure the full file list is considered.

Copilot uses AI. Check for mistakes.
Comment thread CHANGELOG.md Outdated

- New `enterprise/governance-guide.md` documentation page: flagship governance reference for CISO / VPE / Platform Tech Lead audiences, covering enforcement points, bypass contract, failure semantics, air-gapped operation, rollout playbook, and known gaps. Trims duplicated content in `governance.md`, `apm-policy.md`, and `integrations/github-rulesets.md`. Adds `templates/apm-policy-starter.yml`. (#851)
- `apm install` now supports Azure DevOps AAD bearer-token auth via `az account get-access-token`, with PAT-first fallback for orgs that disable PAT creation. Closes #852 (#856)
- New CI safety nets: `merge-gate.yml` orchestrator turns dropped `pull_request` webhook deliveries into clear red checks (currently shadow mode), and `watchdog-stuck-prs.yml` posts a recovery comment on PRs whose required check never dispatched. (PR follow-up to #856 CI flake)
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changelog entry doesn't follow the repo's stated format (one line per PR ending with (#PR_NUMBER)). Please update this line to end with this PR number, consistent with the other entries under ## [Unreleased] > Added.

Suggested change
- New CI safety nets: `merge-gate.yml` orchestrator turns dropped `pull_request` webhook deliveries into clear red checks (currently shadow mode), and `watchdog-stuck-prs.yml` posts a recovery comment on PRs whose required check never dispatched. (PR follow-up to #856 CI flake)
- New CI safety nets: `merge-gate.yml` orchestrator turns dropped `pull_request` webhook deliveries into clear red checks (currently shadow mode), and `watchdog-stuck-prs.yml` posts a recovery comment on PRs whose required check never dispatched. (#PR_NUMBER)

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +88
if: github.event_name == 'pull_request_target'
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p .github/scripts/ci
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This curl fetch is a single point of failure for the whole gate. A transient network/5xx from the GitHub API will fail the job and produce a false-negative red check. Consider adding retries (e.g., --retry with a small backoff) and/or a clearer error message when the script cannot be fetched.

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +91
*)
echo "::error title=Tier 1 failed::Build & Test (Linux) reported '${conclusion}'. See ${url}"
exit 1
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The failure annotation hard-codes Build & Test (Linux) instead of using EXPECTED_CHECK. If the orchestrator is later reused for a different required check name, the message will be misleading; prefer referencing the EXPECTED_CHECK variable consistently in the error text.

Copilot uses AI. Check for mistakes.
Comment thread .github/scripts/ci/watchdog_scan.sh Outdated
Comment on lines +46 to +47
prs=$(gh pr list --repo "$REPO" --state open --limit 200 \
--json number,headRefOid,updatedAt,isDraft,title 2>/dev/null || echo '[]')
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gh pr list failures are fully silenced (2>/dev/null) and treated as an empty PR set, which can hide real watchdog outages (auth regression, API rate limiting, transient GitHub errors). Consider failing the workflow (or at least logging stderr and exiting non-zero) when the PR list cannot be retrieved so maintainers notice the watchdog isn't actually scanning.

Suggested change
prs=$(gh pr list --repo "$REPO" --state open --limit 200 \
--json number,headRefOid,updatedAt,isDraft,title 2>/dev/null || echo '[]')
gh_pr_list_stderr=$(mktemp)
if ! prs=$(gh pr list --repo "$REPO" --state open --limit 200 \
--json number,headRefOid,updatedAt,isDraft,title 2>"$gh_pr_list_stderr"); then
echo "[watchdog] ERROR: failed to list open PRs for ${REPO}" >&2
cat "$gh_pr_list_stderr" >&2
rm -f "$gh_pr_list_stderr"
exit 1
fi
rm -f "$gh_pr_list_stderr"

Copilot uses AI. Check for mistakes.
@danielmeppiel
Copy link
Copy Markdown
Collaborator Author

Dogfood result

The gate workflow ran against this PR itself and passed in 1m34s alongside Build & Test (Linux) (1m19s).

This validates both code paths:

Path Trigger What ran Result
Checkout PR head pull_request actions/checkout of head SHA, then merge_gate_wait.sh pass
Self-bootstrap pull_request_target (would-be) curl base ref, hit 404, degrade to no-op pass (warning emitted)

The bootstrap branch is what makes shipping the orchestrator in the same PR that adds it possible. Once this PR merges to main, all future PRs hit the real script via the base-ref API fetch (no checkout of PR-controlled code on the privileged event).

Ready for review.

- merge_gate_wait.sh: use $EXPECTED_CHECK in failure annotation instead
  of hardcoded 'Build & Test (Linux)' so the orchestrator stays generic.
- merge-gate.yml: add curl --retry/--max-time on the script bootstrap
  fetch so a transient GitHub API blip does not redden the gate.
- watchdog_scan.sh: fail loudly with stderr capture if 'gh pr list'
  errors out, instead of silently treating it as zero PRs (which would
  hide auth regressions or rate limiting).
- watchdog_scan.sh: paginate the changed-files API call so PRs touching
  >100 files cannot be misclassified as docs-only and skipped.
- CHANGELOG: append (#865) to follow the repo convention.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel
Copy link
Copy Markdown
Collaborator Author

Addressed Copilot review feedback (commit 123e193)

All 5 comments adopted:

# File Fix
1 merge_gate_wait.sh Failure annotation now references $EXPECTED_CHECK instead of hardcoded Build & Test (Linux)
2 merge-gate.yml curl bootstrap now uses --retry 5 --retry-delay 3 --retry-connrefused --max-time 30 so a transient API blip doesn't redden the gate
3 watchdog_scan.sh gh pr list errors now exit non-zero with stderr captured to the log (was silently swallowed)
4 watchdog_scan.sh Changed-files query uses gh api --paginate so PRs touching >100 files can't be misclassified as docs-only
5 CHANGELOG.md Appended (#865) per repo convention

Re-validated: shellcheck clean, dry-run against microsoft/apm clean, gate + Build & Test green on this commit.

The watchdog (cron every 15min, posts recovery comments on stuck PRs)
was originally justified for the shadow-mode transition window. Since
we are flipping to required immediately after this PR merges, that
justification disappears.

The merge-gate workflow already triggers on both 'pull_request' and
'pull_request_target' with concurrency dedup, so a single dropped
webhook still fires the gate. The watchdog only added value for the
double-drop case (both webhook channels fail for the same PR), which
is vanishingly rare. We can add it back later if we ever observe one.

Removes:
- .github/workflows/watchdog-stuck-prs.yml
- .github/scripts/ci/watchdog_scan.sh

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel changed the title ci: harden against stuck pull_request webhook deliveries (merge-gate orchestrator + watchdog, shadow mode) ci: add merge-gate orchestrator to harden against dropped pull_request webhooks Apr 23, 2026
@danielmeppiel
Copy link
Copy Markdown
Collaborator Author

Dropped the watchdog (commit eac27d4)

Per discussion: since we flip the gate to required immediately after merge, the watchdog's transition-period justification disappears. The gate's dual-trigger redundancy (pull_request + pull_request_target with concurrency dedup) covers single-webhook drops on its own.

Removed:

  • .github/workflows/watchdog-stuck-prs.yml
  • .github/scripts/ci/watchdog_scan.sh

PR title and body updated accordingly. Re-validation: gate + Build & Test still green on eac27d4.

@danielmeppiel danielmeppiel merged commit 9d2d0e6 into main Apr 23, 2026
10 checks passed
@danielmeppiel danielmeppiel deleted the ci/merge-gate-and-watchdog branch April 23, 2026 08:19
@danielmeppiel
Copy link
Copy Markdown
Collaborator Author

Phase 2 flip executed

Merge Gate / gate is now a required status check on main (ruleset main-protection, id 9294522).

Required checks on main after flip:

Context Source
Build & Test (Linux) ci.yml (kept as backstop)
Build (Linux) ci-integration-pr-stub.yml
Smoke Test (Linux) ci-integration-pr-stub.yml
Integration Tests (Linux) ci-integration-pr-stub.yml
Release Validation (Linux) ci-integration-pr-stub.yml
Merge Gate / gate merge-gate.yml (new)

From now on, any PR that hits the dropped-pull_request failure mode gets a clear red Merge Gate / gate (with recovery instructions in the annotation) instead of a stuck Expected -- Waiting.

Follow-ups (separate PRs, after ~1 week of clean operation)

  1. Drop the 4 stub names (Build (Linux), Smoke Test (Linux), Integration Tests (Linux), Release Validation (Linux)) from required checks if Merge Gate / gate proves stable.
  2. Eventually drop Build & Test (Linux) from required and let Merge Gate / gate be the sole required check on this surface.
  3. Delete ci-integration-pr-stub.yml once stubs are fully removed.

Rollback (if needed): gh api -X PUT repos/microsoft/apm/rulesets/9294522 with Merge Gate / gate removed from required_status_checks.

@danielmeppiel
Copy link
Copy Markdown
Collaborator Author

Correction to my earlier follow-up suggestion

In the flip comment I suggested dropping the 4 stub names (Build (Linux), Smoke Test (Linux), Integration Tests (Linux), Release Validation (Linux)) "after ~1 week of clean operation". That was wrong -- those stubs are architectural, not redundant with Merge Gate.

Re-reading ci-integration-pr-stub.yml: those 4 checks have their real implementation in ci-integration.yml on merge_group events only (i.e., inside the merge queue). The stubs report green at PR time so PRs aren't permanently blocked by checks that only run in the queue. Removing them would brick every PR.

Corrected matrix of required checks on main:

Required check Source Drop?
Build & Test (Linux) ci.yml Could potentially drop later -- Merge Gate echoes its verdict (see below)
Build (Linux) ci-integration-pr-stub.yml (stub for merge_group job) MUST keep
Smoke Test (Linux) ci-integration-pr-stub.yml MUST keep
Integration Tests (Linux) ci-integration-pr-stub.yml MUST keep
Release Validation (Linux) ci-integration-pr-stub.yml MUST keep
Merge Gate / gate merge-gate.yml (this PR) Keep

The only check that could potentially be dropped later is Build & Test (Linux) itself -- Merge Gate already passes when it passes and fails red when it fails or never appears. Dropping it means Merge Gate becomes the sole authority for that surface (less direct UI visibility, cleaner check list). I'd recommend keeping both for ~1 week, then re-evaluating.

danielmeppiel added a commit that referenced this pull request Apr 23, 2026
The dual-trigger pattern (pull_request + pull_request_target with
concurrency cancel-in-progress) shipped in #865 was over-engineered.
It produced TWO 'gate' check-runs per SHA -- one SUCCESS, one
CANCELLED -- and branch protection's status-check rollup treats
CANCELLED as failure, so PRs were silently BLOCKED unless an admin
overrode (which masked the bug on #867).

GitHub Actions has no primitive for 'either of these events
succeeded'. World-class OSS projects (kubernetes, rust, deno, next.js)
accept this and use a single trigger. The cost: a dropped 'pull_request'
webhook (rare; observed once on PR #856) requires manual recovery.

Recovery paths now documented at top of file:
  - push empty commit
  - gh workflow run merge-gate.yml -f pr_number=NNN
  - close + reopen PR

Replaces the dual-trigger + bootstrap-fetch dance with a clean
two-job flow: resolve-sha (handles workflow_dispatch input or PR head)
then gate (sparse checkout + run script). Same script, same exit
codes, same EXPECTED_CHECKS env.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
danielmeppiel added a commit that referenced this pull request Apr 23, 2026
* chore: prepare v0.9.2 release

Bumps version to 0.9.2 and finalizes CHANGELOG with one-line summaries
for each PR merged since 0.9.1.

Highlights:
- ADO AAD bearer-token auth (#856)
- Governance Guide + enterprise docs IA refactor (#851, #858)
- Merge Gate orchestrator + single-authority aggregation (#865, #867)
- Landing + first-package docs rewrite (#855, #866)
- gh-aw imports migration (#864)
- Custom-port surfacing fix (#804)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ci: simplify merge-gate to single pull_request trigger

The dual-trigger pattern (pull_request + pull_request_target with
concurrency cancel-in-progress) shipped in #865 was over-engineered.
It produced TWO 'gate' check-runs per SHA -- one SUCCESS, one
CANCELLED -- and branch protection's status-check rollup treats
CANCELLED as failure, so PRs were silently BLOCKED unless an admin
overrode (which masked the bug on #867).

GitHub Actions has no primitive for 'either of these events
succeeded'. World-class OSS projects (kubernetes, rust, deno, next.js)
accept this and use a single trigger. The cost: a dropped 'pull_request'
webhook (rare; observed once on PR #856) requires manual recovery.

Recovery paths now documented at top of file:
  - push empty commit
  - gh workflow run merge-gate.yml -f pr_number=NNN
  - close + reopen PR

Replaces the dual-trigger + bootstrap-fetch dance with a clean
two-job flow: resolve-sha (handles workflow_dispatch input or PR head)
then gate (sparse checkout + run script). Same script, same exit
codes, same EXPECTED_CHECKS env.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ci: collapse merge-gate into a single job (one check-run in PR UI)

The two-job split (resolve-sha + gate) created two visible check-runs.
Inlining the SHA resolution as a step within the gate job leaves only
one check-run -- 'Merge Gate / gate' -- on the PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants