Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 20 additions & 106 deletions .github/workflows/auto-merge.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
# =============================================================================
# Auto-merge for Trusted PRs
# -----------------------------------------------------------------------------
# Tiers:
# 1. Renovate: Auto-approve + auto-merge
# 2. AI Agents (copilot/jules/claude): Auto-merge when CI passes
# 3. CodeRabbit approved: Auto-merge when CI passes
# 4. Governance bot: Auto-merge action version governance PRs
# 5. Owner PRs: Auto-merge when CI passes
# Common tiers (Dependabot, Renovate, AI Agent, CodeRabbit, Owner) delegate
# to the reusable workflow at ANcpLua/renovate-config. The Governance bot
# tier is qyl-specific and stays local.
#
# Uses pull_request_target for security (runs in base branch context).
# All merges wait for required status checks before completing.
# =============================================================================
# Required secrets:
# AUTOMERGE_APP_ID, AUTOMERGE_APP_PRIVATE_KEY
# See https://github.com/ANcpLua/renovate-config#auto-merge-reusable-workflow

name: Auto-merge

on:
pull_request_target:
types: [ opened, synchronize, reopened, ready_for_review ]
types: [opened, synchronize, reopened, ready_for_review]
pull_request_review:
types: [ submitted ]
types: [submitted]

permissions:
contents: write
Expand All @@ -29,80 +23,9 @@ concurrency:
cancel-in-progress: false

jobs:
renovate-auto-merge:
name: Renovate auto-merge
runs-on: ubuntu-latest
if: >
github.event.pull_request.user.login == 'renovate[bot]' &&
github.event.pull_request.draft == false

steps:
- name: Auto-approve Renovate PRs
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr review --approve "$PR_URL"

- name: Enable auto-merge for Renovate
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr merge --auto --squash "$PR_URL"

ai-agent-auto-merge:
name: AI Agent auto-merge
runs-on: ubuntu-latest
if: |
(github.actor == 'copilot[bot]' || github.actor == 'jules[bot]' || github.actor == 'claude-code[bot]') &&
(
startsWith(github.event.pull_request.head.ref, 'copilot/') ||
startsWith(github.event.pull_request.head.ref, 'jules/') ||
startsWith(github.event.pull_request.head.ref, 'claude/')
)

steps:
- name: Identify AI agent
id: agent
env:
BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
if [[ "$BRANCH" == copilot/* ]]; then
echo "agent=Copilot" >> "$GITHUB_OUTPUT"
elif [[ "$BRANCH" == jules/* ]]; then
echo "agent=Jules" >> "$GITHUB_OUTPUT"
elif [[ "$BRANCH" == claude/* ]]; then
echo "agent=Claude" >> "$GITHUB_OUTPUT"
fi

- name: Auto-approve AI agent PRs
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AGENT: ${{ steps.agent.outputs.agent }}
run: |
echo "Auto-approving $AGENT PR"
gh pr review --approve "$PR_URL" --body "✅ Auto-approved: $AGENT autonomous fix PR"

- name: Enable auto-merge for AI agent PRs
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr merge --auto --squash "$PR_URL"

coderabbit-auto-merge:
name: CodeRabbit auto-merge
runs-on: ubuntu-latest
if: |
github.event_name == 'pull_request_review' &&
github.event.review.state == 'approved' &&
github.event.review.user.login == 'coderabbitai[bot]'

steps:
- name: Enable auto-merge for CodeRabbit approved PRs
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr merge --auto --squash "$PR_URL"
auto-merge:
uses: ANcpLua/renovate-config/.github/workflows/auto-merge-reusable.yml@main
secrets: inherit
Comment on lines +26 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reusable workflow pinned to @main violates SHA-pinning requirement.

External workflow at ANcpLua/renovate-config is referenced by branch, not commit SHA. Combined with secrets: inherit, any upstream change to that workflow immediately receives all repo secrets. The local action on line 38 is correctly SHA-pinned—apply the same discipline here.

Pin to a specific commit:

-    uses: ANcpLua/renovate-config/.github/workflows/auto-merge-reusable.yml@main
+    uses: ANcpLua/renovate-config/.github/workflows/auto-merge-reusable.yml@<commit-sha> # vX.Y.Z
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/auto-merge.yml around lines 26 - 28, The reusable workflow
reference under the "auto-merge" job is pinned to a branch
(ANcpLua/renovate-config/.github/workflows/auto-merge-reusable.yml@main); update
that reference to a specific commit SHA instead (replace `@main` with
@<commit-sha>) so the job is SHA-pinned, and keep the existing secrets: inherit
setting; fetch the exact commit SHA from the ANcpLua/renovate-config repo (e.g.,
using git ls-remote or the GitHub UI) and use it in the uses: value to lock the
external workflow to that commit.


governance-auto-merge:
name: Governance bot auto-merge
Expand All @@ -111,12 +34,17 @@ jobs:
github.event.pull_request.user.login == 'github-actions[bot]' &&
github.event.pull_request.head.ref == 'chore/action-governance' &&
github.event.pull_request.draft == false

steps:
- uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v2.1.4
id: app-token
with:
app-id: ${{ secrets.AUTOMERGE_APP_ID }}
private-key: ${{ secrets.AUTOMERGE_APP_PRIVATE_KEY }}

- name: Verify only .github files changed
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
files=$(gh pr diff "$PR_URL" --name-only)
Expand All @@ -129,27 +57,13 @@ jobs:
- name: Auto-approve governance PR
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh pr review --approve "$PR_URL" \
--body "✅ Auto-approved: action governance update (scoped to .github/)"

- name: Enable auto-merge
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr merge --auto --squash "$PR_URL"

owner-auto-merge:
name: Owner auto-merge
runs-on: ubuntu-latest
if: >
github.event.pull_request.user.login == github.event.repository.owner.login &&
github.event.pull_request.draft == false

steps:
- name: Enable auto-merge for owner PRs
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: gh pr merge --auto --squash "$PR_URL"
26 changes: 12 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,14 @@ Endpoint implementations under `services/qyl.collector/**Endpoints.cs` track `co

## HTTP client error handling

In `services/qyl.loom/CollectorClient.cs` and any other `HttpClient`-consuming code, the established pattern:
In `services/qyl.loom/CollectorClient.cs` and any other `HttpClient`-consuming code:

- Before `ReadFromJsonAsync` on a non-success response, check
`Content.Headers.ContentType?.MediaType == "application/json"` and `ContentLength > 0`; otherwise fall back to
`StatusCode`.
- `ReadFromJsonAsync` is wrapped in `try/catch (JsonException)` on non-success paths.
- Failures return a structured DTO. Throwing on expected HTTP error status is the exception, not the rule.
- Default: `response.EnsureSuccessStatusCode()` → `ReadFromJsonAsync(CollectorClientJsonContext.Default.<Type>, ct)`.
Let unexpected HTTP failures throw; the AOT-friendly `JsonTypeInfo` context is mandatory — never the reflection
overload.
- For endpoints with a documented failure body (e.g. `CreatePullRequestAsync` at `CollectorClient.cs:148-178`),
branch on `IsSuccessStatusCode` and read either the success or failure DTO. Reserve this shape for contracts that
actually define a structured error payload.

## Codegen boundaries

Expand Down Expand Up @@ -195,9 +196,6 @@ In `services/qyl.mcp/`:

For `services/qyl.loom/`, `services/qyl.loom.patterns/`, `services/qyl.mcp/Agents/`.

References worth reading before editing agent/workflow code: `~/.claude/skills/microsoft-agent-framework-qyl/SKILL.md` (
qyl overlay) and `~/.claude/skills/microsoft-agent-framework/SKILL.md` (core MAF).

Local invariants:

- qyl three-builder pattern: `IXxxChatClientBuilder` → `IXxxAgentsBuilder` → workflow. One `Build*Agent()` factory per
Expand All @@ -214,14 +212,14 @@ Reach for these before hand-rolling. Each row points at a concrete qyl call-site

| Layer | Entry point | qyl call-site |
|-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| **Agent — standalone** | `llm.AsAIAgent(new ChatClientAgentOptions { Name, Description, ChatOptions = new() { Instructions } }).AsBuilder().UseQylAgentTelemetry().Build()` | `services/qyl.loom/Autofix/Workflow/Executors/RcaExecutor.cs:35-40` and every sibling executor |
| **Agent — non-streaming** | `await agent.RunAsync(userMessage, cancellationToken: ct)` | `RcaExecutor.cs:42` — the universal shape across Autofix executors and `TriagePipelineService` |
| **Agent — streaming** | `await foreach (var evt in streamingRun.WatchStreamAsync(ct)) { … }` | `services/qyl.loom/Autofix/AutofixAgentService.cs:66-77`, `services/qyl.loom/Exploration/ExplorationOrchestrator.cs:37` |
| **Agent — standalone** | `llm.AsAIAgent(new ChatClientAgentOptions { Name, Description, ChatOptions = new() { Instructions } }).AsBuilder().UseQylAgentTelemetry().Build()` | `services/qyl.loom/Agents/QylLoomAgentsBuilder.cs:82-85` (every `Build*Agent()` factory) |
| **Agent — non-streaming** | `await agent.RunAsync(userMessage, cancellationToken: ct)` | `services/qyl.loom/Autofix/Workflow/Executors/HypothesisExecutor.cs:38-40` — universal shape across Autofix executors |
| **Agent — streaming** | `await foreach (var evt in streamingRun.WatchStreamAsync(ct)) { … }` | `services/qyl.loom/Autofix/LoomAutofixRunner.cs:188`, `services/qyl.loom/Exploration/ExplorationOrchestrator.cs:37` |
| **Agent — structured output** | `await agent.RunAsync<T>(prompt)` → `AgentResponse<T>.Result` | When `T` is a `LoomToolEnvelope<TData>` verdict (see `services/qyl.mcp/Tools/`) |
| **Session** | `agent.CreateSessionAsync()` • `SerializeSessionAsync` / `DeserializeSessionAsync` | When the same agent must preserve context across MCP tool calls — gate on `LoomRunState` |
| **Tools — local** | `AIFunctionFactory.Create(methodInfo, instanceFactory, new AIFunctionFactoryOptions { Name, ... })` | `internal/qyl.instrumentation/Instrumentation/Loom/LoomToolFactoryBridge.cs:99-119` |
| **Tools — local** | `AIFunctionFactory.Create(methodInfo, new AIFunctionFactoryOptions { Name = "qyl.<area>.<verb>" })` | `services/qyl.loom/Autofix/Workflow/AutofixContextToolFactories.cs:9-15` |
| **Workflow — build** | `new WorkflowBuilder(start).AddEdge(a, b).AddFanOutEdge(src, [t1, t2, t3]).WithOutputFrom(last).Build()` | `services/qyl.loom/Autofix/Workflow/AutofixWorkflowFactory.cs:44-51`, `services/qyl.loom/Exploration/Workflow/ExplorationWorkflowFactory.cs:23-28` |
| **Workflow — run** | `InProcessExecution.RunStreamingAsync(workflow, input)` + `run.WatchStreamAsync(ct)` | `AutofixAgentService.cs:66`, `ExplorationOrchestrator.cs:37` |
| **Workflow — run** | `InProcessExecution.RunStreamingAsync(workflow, input)` + `run.WatchStreamAsync(ct)` | `services/qyl.loom/Autofix/LoomAutofixRunner.cs:182-188`, `services/qyl.loom/Exploration/ExplorationOrchestrator.cs:37` |
| **Observability** | `IChatClient` decoration (`.WithQylTelemetry` short form or `.UseQylTelemetry` on `ChatClientBuilder` fluent form) **and** `agent.AsBuilder().UseQylAgentTelemetry().Build()` on `AIAgent`. Wrap both layers — wrapping one halves the spans. | All executors + `internal/qyl.instrumentation/Instrumentation/GenAi/GenAiInstrumentation.cs:53,100,141` |

## MAF.Advanced.Patterns — consume, don't duplicate
Expand Down
2 changes: 1 addition & 1 deletion Version.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<NukeVersion>10.1.0</NukeVersion>
<NetArchTestVersion>1.3.2</NetArchTestVersion>
<YamlDotNetVersion>16.3.0</YamlDotNetVersion>
<SpectreConsoleVersion>0.55.0</SpectreConsoleVersion>
<SpectreConsoleVersion>0.55.2</SpectreConsoleVersion>
<GitVersionToolVersion>6.5.1</GitVersionToolVersion>
<ReportGeneratorVersion>5.5.1</ReportGeneratorVersion>
<VersionizeVersion>2.5.0</VersionizeVersion>
Expand Down
Loading
Loading