diff --git a/.agents/skills/memory-leak-fixer/SKILL.md b/.agents/skills/memory-leak-fixer/SKILL.md new file mode 100644 index 00000000000..ecadbe05579 --- /dev/null +++ b/.agents/skills/memory-leak-fixer/SKILL.md @@ -0,0 +1,307 @@ +--- +name: memory-leak-fixer +description: > + Scan SkiaSharp for native ownership / disposal memory leaks AND fix them with a + red→green regression test. Two combined modes in one skill: (1) SCAN — hunt the + SkiaSharp leak signature (undisposed SKObject handle, wrong `owns:` flag, + same-instance double-dispose, unremoved event/handler subscription, + `fixed`-pointer lifetime) and empirically confirm it; (2) FIX — write + a failing regression test, implement the minimal idiomatic fix, prove it goes green, + and open a PR. + + Triggers: "memory leak", "leak scan", "leak hunter", "find leaks", "disposal bug", + "not disposed", "undisposed handle", "owns flag", "double free", "AccessViolation on + dispose", "native memory grows", "handle leak", "GC does not collect SKObject", + "fix the leak", any request to proactively find or fix SkiaSharp memory/disposal leaks. + + For a user-reported functional bug that is not a leak, use `issue-fix` instead. +--- + +# Memory Leak Fixer + +Proactively **find** and **fix** memory leaks in SkiaSharp — a thin managed +wrapper over native Skia, so its recurring, high-impact leak family is **native +ownership / disposal correctness** — the C# binding failing to dispose, own, pin, or root a +native object correctly — not the managed view-retention leaks a pure-managed app framework +worries about. This skill hunts that family and produces a validated fix. + +**Scope: managed C# only.** Work only in the code SkiaSharp owns — the C# bindings +(`binding/**`) and view layers (`source/**`). Everything under `externals/skia/**` is +upstream Skia (including our C shim): out of scope, not buildable on a standard runner, and +handled by a separate process. Every candidate must be provable and fixable from C#. + +Read [`documentation/dev/memory-management.md`](../../../documentation/dev/memory-management.md) +first — it is the authoritative model (pointer types, `owns:` flag, ref-count rules, the +same-instance-return contract). This skill assumes that model. + +The leak catalogue this skill scans against — **11 real families**, each with a description, +why it's bad, a leak→fix code example, and a leak-specific anti-pattern — is in +[`references/types-of-leaks.md`](references/types-of-leaks.md). Read it before scanning +(Phase 1) and consult the matching family when writing a fix (Phase 3). + +## Golden rules (non-negotiable) + +1. **One leak per run.** Pick the single strongest candidate; do not batch. +2. **Empirical confirmation before any fix.** A fix is only valid if a regression test + **FAILS on the current tree without the fix and PASSES with it** (red→green, both + directions). No demonstrated red→green ⇒ no PR. +3. **Never weaken, skip, mute, `[Obsolete]`-hide, or delete a test to make it pass.** If + the only thing that turns green is a mute, the fix is wrong — reject it. +4. **Never edit generated files or upstream Skia.** `*.generated.cs` and everything under + `externals/skia/**` (including our C shim) are off-limits — this skill is **managed-C# + only**. Every fix lives in `binding/**` or `source/**`. +5. **ABI stability.** Add overloads / methods; never change or remove a public signature. +6. **Honest scope note.** In every issue/PR, say whether it is a clear framework bug or a + usage footgun the framework could harden — and what is *empirically proven* vs + *statically reasoned*. +7. **Finding nothing is the expected outcome.** SkiaSharp is mature and heavily hardened; + there is **no planted/seeded bug** waiting to be found. Most runs should end with **no + candidate**. Never invent a leak, never rationalize deliberately-hardened, well-documented + code as "decoys," and never lower the evidence bar to force a result. A quiet run is a + first-class success — report it and emit a single `noop` (see Phase 5). + +--- + +## Mode selection + +Run the phases in order. The skill has two entry points: + +| You were asked to… | Start at | Notes | +|---|---|---| +| Find a leak (scan only) / file an issue | Phase 1 → 2 → (report) | Stop after confirmation; file `[memory-leak]` issue. | +| Scan **and** fix (the default, and what the workflow does) | Phase 0 → 1 → 2 → 3 → 4 → 5 | End-to-end: hunt → prove → fix → **file the finding as an issue and open a linked draft PR** that closes it (`Fixes #…`). | + +--- + +## Phase 0 — Setup + +> **CI runner reality:** this skill fixes **managed C# only** (`binding/**`, `source/**`). +> The native library is consumed as a **pre-built package** (`externals-download`) — you +> never build native code, so every candidate must be provable and fixable from C#. A leak +> whose only fix is in native / upstream Skia is out of scope: file an issue (Phase 4), +> never open a PR you cannot validate. + +1. Confirm the SDK: `dotnet --version`. +2. Restore the pre-built natives so the C# projects build and the tests run: + + ```bash + dotnet cake --target=externals-download # pre-built natives for managed-C# work + ``` + +--- + +## Phase 1 — Scan (find ONE candidate) + +### 1.1 Choose a focus area (round-robin across runs) +A full 11-family sweep every run is wasteful and the surface is mostly hardened, so start from +ONE focus family and widen only if it's exhausted. Rotate the *starting* family on a +time-based **round-robin** so consecutive runs cover different families. This needs only +`date`, so it behaves identically locally and in CI — no `$GITHUB_RUN_NUMBER` / `$RANDOM` +(which don't exist or aren't deterministic outside GitHub Actions): + +```bash +# Round-robin: advance one family every hour, cycling through all 11. +DOY=$(date -u +%j); HOUR=$(date -u +%H) # day-of-year + hour, both zero-padded +FOCUS=$(( (10#$DOY * 24 + 10#$HOUR) % 11 )) # 10# forces base-10 +echo "focus family: $FOCUS" +``` +The `10#` prefix is **required**: `date` zero-pads `%j`/`%H`, and `$(( 08 ))` is an +invalid-octal error without it. For a targeted local run, skip the rotation and just name the +family you want. + +Every family is drawn from a **real, historical SkiaSharp leak fix**. Now open +**[references/types-of-leaks.md](references/types-of-leaks.md)** and load family `#FOCUS`: its +**Where to look** line gives the path + grep starting points, and the rest of the entry is the +description, why-it's-bad, a leak→fix example, and the per-family anti-pattern. **Read that +family before scanning.** If it's exhausted (its leaks are already open issues/PRs — see 1.3), +advance to the next index and load that family. + +### 1.2 Establish the retention/ownership path +For each candidate write the precise path **with `file:line` citations**: +- Native-handle leaks: `creation site → escape path → missing Dispose/unref`. +- `owns:` bugs: the P/Invoke name that produced the handle (`_new_`/`_create` returns an + owned object; `_get_`/property-style returns a borrowed pointer) vs the `owns:` value the + C# wrapper passed. +- Views retention: `long-lived root → subscription/handler → transient view`, and the + unload path that should have detached but doesn't. + +**Skip if already weak/correct:** uses `WeakEventHandler`/`WeakReference`/ +`ConditionalWeakTable`, or the ownership already matches the memory-management rules. + +### 1.3 De-dup against this project's own open issues/PRs +Before confirming, fetch and skip anything already covered. **Search two ways** — real +SkiaSharp leak fixes are usually filed as `[BUG] …` (not `[memory-leak] …`), so the +title-prefix search alone will miss them. Also search by the specific **type/API name**: + +```bash +# 1. Prior runs of THIS workflow (our own prefix): +gh issue list --repo "$GITHUB_REPOSITORY" --search '"[memory-leak]" in:title' \ + --state open --limit 100 --json number,title,body +gh pr list --repo "$GITHUB_REPOSITORY" --search '"[memory-leak]" in:title' \ + --state open --limit 100 --json number,title,body + +# 2. Human-reported coverage of the SAME api/type (the important check): +# e.g. for the Blob.FromStream candidate below, this surfaces open PR #3473. +gh issue list --repo "$GITHUB_REPOSITORY" --search 'Blob.FromStream in:title,body' --state open --json number,title +gh pr list --repo "$GITHUB_REPOSITORY" --search 'Blob.FromStream in:title,body' --state open --json number,title +``` + +A candidate is OUT only if an **open** issue/PR already covers the same +handle / ownership path (by our prefix OR by the api/type name). A candidate whose only +prior item is CLOSED may be re-filed. **Worked example:** the `HarfBuzzSharp.Blob.FromStream` +`fixed`-pointer leak (family 4) is a genuine, still-present bug — but open PR #3473 "Make +Blob.FromStream GC safe" already fixes it, so it is OUT: stand down, do **not** open a +duplicate PR, emit a `noop`. + +Pick the ONE strongest candidate. If none is convincing, **stop** — a quiet run is a success +(the surface is hardened; the value is catching *new* leaks as code lands). Do not keep +digging past a reasonable single pass hoping to manufacture a finding: report the quiet result +and emit a `noop` (Phase 5). + +--- + +## Phase 2 — Prove (empirical confirmation) + +Every in-scope leak is observable from **managed** code, so prove it with a `WeakReference` + +forced-GC probe that mirrors the existing memory tests. Prove it against the **shipped +`SkiaSharp` NuGet** in a throwaway project — no source build, no display: + +```bash +mkdir -p /tmp/leakprobe && cd /tmp/leakprobe +``` + +`leakprobe.csproj` referencing `` — the +floating `*` resolves to the **latest stable** SkiaSharp on nuget.org automatically (use +`Version="*-*"` to include the latest preview) — plus `xunit` + `Microsoft.NET.Test.Sdk`, +then a single `[Fact]` that: +1. runs **Control** (correct usage), **Leaky** (the suspect path), **Mitigation** (the + proposed workaround), each allocating N subjects tracked by `WeakReference`; +2. forces GC (`for (i=0;i<6;i++){ GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); }`); +3. asserts the leaky subjects stay **alive** while control + mitigation are **collected**. + +```bash +cd /tmp/leakprobe && dotnet test --logger "console;verbosity=normal" +``` + +- Passes ⇒ leak confirmed. Failed to build / assertions don't hold ⇒ hypothesis wrong; + iterate once on another candidate or stop. +- For undisposed-handle leaks, prefer proving the wrapper is **not collected / + `Dispose` is never reached**; the in-repo equivalent uses `SKObject.GetInstance(handle, + out _)` + `CollectGarbage()` (see `tests/Tests/SkiaSharp/SKObjectTest.cs`). + +--- + +## Phase 3 — Fix (red→green regression test + minimal change) + +### 3.1 Write the failing test FIRST (red) +Add a focused regression test to the console test project (source lives under `tests/Tests/`, +run via `tests/SkiaSharp.Tests.Console`). Model it on existing disposal/leak tests: +- `AssertEx.EventuallyGC(weakRef, …)` — GC-based (`tests/Tests/Xunit/AssertEx.cs`). +- `SKObject.GetInstance(handle, out inst)` + `CollectGarbage()` — wrapper lifecycle + (`tests/Tests/SkiaSharp/SKObjectTest.cs`). +- For views: the handler pattern in + `tests/SkiaSharp.Tests.Devices/Tests/Maui/MemoryLeakTests.cs`. + +Build and **confirm the test FAILS** on the current tree (proves it catches the leak): + +```bash +dotnet cake --target=externals-download # pre-built natives +dotnet build binding/SkiaSharp/SkiaSharp.csproj +dotnet test tests/SkiaSharp.Tests.Console/SkiaSharp.Tests.Console.csproj --filter "FullyQualifiedName~" +``` + +If a test you *expected* to be red is green, your hypothesis is wrong — go back to Phase 1. + +### 3.2 Implement the minimal idiomatic fix +Apply the **Fix (✓)** for the matching family in +[`references/types-of-leaks.md`](references/types-of-leaks.md) — every family has a worked +before/after there. Then re-read that family's **Watch out (❌ don't):** note: it names the +specific *wrong fix* that turns one leak into another (an unconditional `Dispose`, flipping +`owns:` blind, nulling a field before disposing, a pinned `GCHandle` where a plain field +suffices, …). + +Touch only the minimal code, and keep it inside `binding/**` / `source/**`. Never change a +public signature to fix ownership — add an overload or fix internals (ABI stability). If the +only correct fix is in native / upstream Skia, **stop and file an issue** (Phase 4) — this +skill does not open native PRs. + +### 3.3 Confirm green + no regressions +Rebuild and re-run the regression test (now PASSES) plus neighbouring tests: + +```bash +dotnet test tests/SkiaSharp.Tests.Console/SkiaSharp.Tests.Console.csproj --filter "FullyQualifiedName~" +# then a wider relevant slice, e.g. the type's test class, to catch regressions +``` + +Enforce red→green **in both directions**: revert the fix ⇒ red; re-apply ⇒ green. + +### 3.4 Self-review gate — before you open the PR +Run this checklist **before** committing or opening the PR, so a bad attempt is dropped now +instead of pushed and reverted. If any box can't be ticked, **fix it or stand down** (emit a +`noop`) — do not open the PR. + +- [ ] The test genuinely goes **red without the fix, green with it**, both directions + (§3.3) — not green because a test was muted, `[Obsolete]`-hidden, skipped, or weakened. +- [ ] The fix is inside `binding/**` / `source/**` only — no `*.generated.cs`, no + `externals/skia/**`, no native / upstream change. +- [ ] **No public signature changed** — overloads / internals only (ABI stable). +- [ ] The matching family's **Watch out (❌ don't):** note in + [`references/types-of-leaks.md`](references/types-of-leaks.md) does **not** describe + what you just did (no unconditional same-instance `Dispose`, no blind `owns:` flip, no + field nulled before dispose, no pinned `GCHandle` where a plain field suffices, …). +- [ ] This is a real leak with a citable path — not hardened, documented code rationalised + as a "decoy," and not a finding manufactured to avoid a quiet run. +- [ ] Not already covered by an open issue/PR (§1.3). + +All ticked ⇒ proceed to Phase 4 (file the finding, then open the PR). Any unticked ⇒ no PR +(fix it, file the finding as an issue on its own, or `noop`). + +--- + +## Phase 4 — File the finding, then open the linked fix PR + +A confirmed, managed-C#-fixable leak produces **two linked safe outputs** so the *finding* and +the *fix* are tracked separately and the issue **auto-closes when the PR merges**. + +### 4.1 The issue — the finding +Emit a `create_issue` that describes the **leak, not the fix**. Give it a `temporary_id` +(format `aw_` + 3–8 alphanumeric characters — no underscores or other symbols — e.g. `aw_leak1`) +so the PR can reference it +before its real number exists. Body (markdown): +- **AI-generated banner** naming this workflow + the `memory-leak-fixer` skill. +- **Family**, and the **retention/ownership path** with `file:line` citations. +- **Evidence**: the Phase 2 proof — the probe you ran and its alive/collected counts. +- **Scope note**: framework bug vs footgun; empirically-proven vs statically-reasoned; ABI impact. + +### 4.2 The PR — the fix +Create a feature branch (`dev/memory-leak-`), commit the test + fix, and open a +**draft** `create_pull_request`. Body (markdown): +- **AI-generated banner** naming this workflow + skill. +- **The fix**: what changed and why it is the idiomatic pattern (point at the family's `Fix ✓`). +- **Proof (red→green)**: the failing-then-passing test and the exact `dotnet test` commands. +- **A closing keyword on its own line so merging auto-closes the finding:** `Fixes #` + — e.g. `Fixes #aw_leak1` (the id you gave the issue in 4.1). gh-aw rewrites it to the real issue + number once the issue is created. + +### 4.3 Out of scope (native / upstream only) +If the leak is real but the only correct fix lives under `externals/skia/**` (incl. the C +shim), do **not** open a PR. Emit the `create_issue` from 4.1 **alone** — finding plus the +proposed native fix — so nothing is lost. + +--- + +## Phase 5 — Report + +Write a short summary: which family, the candidate (`file:line`), the proof result, and the +resulting issue + PR links. When run from the agentic workflow, append this to the run's step +summary. + +**End with the right safe output(s):** +- **Confirmed + managed-C# fix** → the **issue + PR pair** from Phase 4 (the PR body carries + `Fixes #…` so merging closes the issue). +- **Confirmed but native/upstream-only fix** → the **`create-issue`** alone (finding + proposal). +- **Quiet run** (no convincing candidate) **or** a **dry run** → a single **`noop`** carrying + this summary. + +A `noop` is the correct "nothing to do / analysis only" signal — never finish with no safe +output, which makes the run look incomplete. diff --git a/.agents/skills/memory-leak-fixer/references/types-of-leaks.md b/.agents/skills/memory-leak-fixer/references/types-of-leaks.md new file mode 100644 index 00000000000..92f86db0704 --- /dev/null +++ b/.agents/skills/memory-leak-fixer/references/types-of-leaks.md @@ -0,0 +1,481 @@ +# SkiaSharp leak types — reference + +The catalogue the `memory-leak-fixer` skill scans against. **Every family below is drawn +from a real, historical SkiaSharp fix** (issue/PR cited), so the hunt targets patterns that +have actually shipped as bugs in this repo — not hypotheticals. + +**Scope: managed C# only.** This skill hunts and fixes leaks in the code SkiaSharp owns — +the C# bindings (`binding/**`) and view layers (`source/**`). The native Skia C/C++ under +`externals/skia/**` (including our C shim) is **out of scope**: it is upstream, cannot be +built or validated on a standard runner, and its fixes go through a different process. Every +family here is therefore something you can prove and fix from C#. + +Read this alongside [`documentation/dev/memory-management.md`](../../../../documentation/dev/memory-management.md), +which is the authoritative ownership model (pointer types, `owns:` flag, ref-count rules, +the `HandleDictionary`, and the same-instance-return contract). This file adds, per family: +**where to look → what it is → why it's bad → a leaking example → the idiomatic fix → a +watch-out.** + +Code samples are illustrative and trimmed to the essential lines; real wrappers add +argument validation and `GC.KeepAlive`. `✓` = correct, `❌` = the bug. + +Each family ends with a **Watch out (❌ don't):** note — the leak-specific *wrong fix* that +turns one bug into another. Re-read the matching one during the pre-PR self-review gate. + +Quick index (the `#` is the rotating focus index the skill uses): + +| # | Family | One-line signature | +|--:|---|---| +| 0 | Undisposed native handle | owned/ref-counted `SKObject` escapes a factory/cache and is never disposed | +| 1 | Wrong `owns:` flag | borrowed pointer wrapped `owns:true` (double-free) or owned handle `owns:false` (leak) | +| 2 | Same-instance double-dispose | `Subset`/`ToRasterImage`-style self-return disposed twice | +| 3 | Managed retention (Views) | event/handler subscribed but never torn down; `base.Dispose` not chained | +| 4 | `fixed`-pointer lifetime | a `fixed` pointer stored by native code beyond the block | +| 5 | Finalizer / collection ordering | child holds a raw pointer into a parent that can be collected first | +| 6 | Clone / copy double-free | `Clone()` shares one native pointer across two wrappers | +| 7 | Disposing native statics/singletons | an immortal native object reached via a non-protected cache is unref'd | +| 8 | Field not nulled on dispose | disposed native child left referenced → double-dispose / graph retained | +| 9 | Stream / callback / delegate-proxy lifetime | `GCHandle`/proxy freed too early (dangling) or never (leak) | +| 10 | Allocation-failure path | wrapper returned even when native create failed, or half-built object leaked | + +--- + +## 0 — Undisposed native handle + +**Where to look.** `binding/SkiaSharp/**`. `grep -rnE "GetObject\(|new SK[A-Za-z]+\(" binding/SkiaSharp`, then trace ownership of each hit through to a `Dispose`/`using`. + +**What it is.** A factory, getter, or cache mints an *owned* or *ref-counted* `SKObject` +(pixels, GPU resources, font tables, encoded data) that escapes without ever being disposed +— or is parked in a static/instance cache that is never cleared. + +**Why it's bad.** The native allocation is only reclaimed by the finalizer, which runs +non-deterministically and late. Under load (per-frame image decode, GPU surfaces) native +memory and GPU handles pile up far faster than the finalizer frees them → native OOM, +GPU resource exhaustion, or a monotonically growing process while managed heap looks fine. + +**Leak (❌):** +```csharp +// Decodes a fresh SKImage every frame and drops it on the floor. +foreach (var frame in frames) { + var image = SKImage.FromEncodedData(frame); // owns a native SkImage + canvas.DrawImage(image, 0, 0); + // image never disposed → native pixels accumulate until finalization +} +``` + +**Fix (✓):** +```csharp +foreach (var frame in frames) { + using var image = SKImage.FromEncodedData(frame); + canvas.DrawImage(image, 0, 0); +} // native pixels freed deterministically at end of scope +``` +For a cache, dispose evicted entries and clear the cache on teardown. + +**Watch out (❌ don't):** don't slap `using`/`Dispose` on a handle you don't actually own — +a *borrowed getter* result (family 1), a *same-instance return* (family 2), or a +*process-wide singleton* (family 7). Confirm the object is genuinely owned before disposing, +or you convert a leak into a double-free. + +**Real cases:** the general class behind many reports; see `documentation/dev/memory-management.md`. + +--- + +## 1 — Wrong `owns:` flag + +**Where to look.** `binding/SkiaSharp/**`. `grep -rnE "owns: *(true|false)|GetOrAddObject" binding/SkiaSharp`, then match each against the P/Invoke name that produced the handle. + +**What it is.** The `owns:` argument to `GetObject`/the wrapper ctor doesn't match the +ownership contract: a *borrowed* pointer (a `_get_` getter that returns an internal pointer) +is wrapped `owns:true`, or an *owned* handle (a `_new_`/create that returns a fresh object) +is wrapped `owns:false`. + +**Why it's bad.** `owns:true` on a borrowed pointer → the wrapper's `DisposeNative` deletes +or unrefs an object it doesn't own → **double-free / `AccessViolationException`**, often in +unrelated code later. `owns:false` on an owned handle → **the object is never freed → leak**. + +**Leak (❌):** +```csharp +// sk_foo_get_bar returns a BORROWED pointer owned by the parent foo. +public SKBar Bar => + GetObject(SkiaApi.sk_foo_get_bar(Handle), owns: true); // ❌ double-free +``` + +**Fix (✓):** +```csharp +public SKBar Bar => + GetObject(SkiaApi.sk_foo_get_bar(Handle), owns: false); // borrowed → don't free +``` +Conversely, a `sk_bar_new(...)` result is a fresh object and must be `owns: true`. + +**Watch out (❌ don't):** don't guess the flag or flip it to make a crash/leak "go away." +Read the P/Invoke name: `_new_`/`_create` returns owned → `owns:true`; `_get_`/property-style +accessors return borrowed → `owns:false`. Getting this backwards just swaps a leak for a +crash. When the contract is genuinely unclear from the managed side, file an issue rather +than flipping blind. + +**Real cases:** the counterpart of family 7 (dispose-protected singletons); verify each new +getter against whether it returns a fresh ref or a borrowed pointer. + +--- + +## 2 — Same-instance double-dispose + +**Where to look.** `binding/SkiaSharp/**`. `grep -rnE "Subset|ToRasterImage|== source|!= source" binding/SkiaSharp` — any method that can return `this`. + +**What it is.** Some methods may return the **same** instance rather than a new one — +`SKImage.Subset` (can return `this`), `SKImage.ToRasterImage(ensurePixelData:false)`, +`SKImage.Encode` (routes through `ToRasterImage`). A caller that disposes both the source and +the "result" then disposes the same native object twice. + +**Why it's bad.** Double-free → `AccessViolationException`, or the source is destroyed out +from under the caller who still needs it. + +**Leak/crash (❌):** +```csharp +var raster = image.ToRasterImage(); // may return `image` itself +raster.Encode(...); +raster.Dispose(); +image.Dispose(); // ❌ if raster == image, second free crashes +``` + +**Fix (✓):** the framework guards this internally, and callers should too: +```csharp +var raster = image.ToRasterImage(); +raster.Encode(...); +if (raster != image) // never dispose a same-instance return twice + raster.Dispose(); +image.Dispose(); +``` +Framework-side pattern (see `SKImage.Encode`): `if (this != raster) raster.Dispose();`. + +**Watch out (❌ don't):** don't add an *unconditional* `result.Dispose()` — the reference +check `if (result != source)` is the whole fix; dropping it re-introduces the double-free. +And don't dispose the source before you're finished with the result, since they may be the +same object. + +**Real cases:** the `Subset`/`ToRasterImage` contract in `documentation/dev/memory-management.md`. + +--- + +## 3 — Managed retention (Views / handlers) + +**Where to look.** `source/SkiaSharp.Views*/**`. `grep -rnE "\+= |event |WeakReference|base\.Dispose|Detach" source/SkiaSharp.Views*` — every `+=` needs a matching teardown. + +**What it is.** In `source/SkiaSharp.Views*`: a handler, control, or renderer subscribes to +an event (`PaintSurface`, `PropertyChanged`, an invalidation ticker, a platform peer callback) +in a ctor / `Connect` / `Loaded`, but the matching `-=` / `Disconnect` / `Unloaded` / +`Dispose` is missing. Or a derived control's `Dispose(bool)` never chains `base.Dispose(bool)` +when the base owns native resources. + +**Why it's bad.** The long-lived event *source* now roots the transient view, so the whole +visual subtree — and the native surfaces/GL contexts it owns — is never collected. Repeated +navigation leaks a surface each time. + +**Leak (❌):** +```csharp +public MyCanvasControl() +{ + _ticker.Tick += OnTick; // subscribe +} +protected override void Dispose(bool disposing) +{ + _surface?.Dispose(); + // ❌ _ticker.Tick -= OnTick never happens → ticker roots `this` forever +} +``` + +**Fix (✓):** +```csharp +protected override void Dispose(bool disposing) +{ + if (disposing) + _ticker.Tick -= OnTick; // symmetric teardown + _surface?.Dispose(); + base.Dispose(disposing); // chain if the base owns native resources +} +``` + +**Watch out (❌ don't):** don't unsubscribe from inside a finalizer — a finalizer must not +touch other managed objects (the event source may already be finalized). Do the `-=` in +`Dispose(bool disposing)` under `if (disposing)`. And don't forget to chain +`base.Dispose(disposing)` — a subtle leak that looks fixed but isn't. + +**Real cases:** #3309, #2955, #2472, #1095 — event/handler teardown and `base.Dispose(bool)` +chaining fixes across the WPF / Forms / MAUI view layers. + +--- + +## 4 — `fixed`-pointer lifetime + +**Where to look.** `binding/**` and `source/**`. `grep -rnE "fixed *\(" binding source`, then check whether the native call copies the buffer or retains the pointer past the block. + +**What it is.** A `fixed` block produces a pointer into a managed array and hands it to native +code that **stores** the pointer (a non-copying mode) and outlives the block. Once the block +exits, the array is unpinned. + +**Why it's bad.** After `fixed` ends the GC is free to move or collect the array, but native +code still holds the old address → **use-after-free / silent data corruption** under GC +pressure. Intermittent, load-dependent, extremely hard to reproduce. + +**Leak (❌):** a non-copying native API stores the pointer, but the array is unpinned the +moment the `fixed` block exits: +```csharp +byte[] data = GetManagedBuffer(); +fixed (byte* ptr = data) +{ + // ❌ native keeps `ptr`, yet `data` is free to move/collect once this block ends + return new SKNativeThing(ptr, data.Length, copy: false, () => { /* release */ }); +} +``` + +**Fix (✓):** pin stably with a `GCHandle` and free it only when native releases the object: +```csharp +byte[] data = GetManagedBuffer(); +var handle = GCHandle.Alloc(data, GCHandleType.Pinned); // stable pin; GC can't move it +return new SKNativeThing(handle.AddrOfPinnedObject(), data.Length, + copy: false, () => handle.Free()); // freed in the release callback +``` +(Or have the native API copy the buffer, so no pin is needed at all.) + +**Watch out (❌ don't):** don't "fix" this by adding `GC.KeepAlive(data)` *inside* the `fixed` +block — the pointer escapes the block, so KeepAlive there proves nothing. And don't free the +`GCHandle` before native is finished with the memory; free it in the release delegate. + +**Real cases:** #3472 / PR #3473 (a `fixed`-pointer that escapes into a non-copying native +API); the ownership model in `documentation/dev/memory-management.md`. + +--- + +## 5 — Finalizer / collection ordering + +**Where to look.** `binding/SkiaSharp/**`. `grep -rnE "GC.KeepAlive|internal .* Handle" binding/SkiaSharp`, and compare sibling wrappers — one that keeps no parent field where the others do is suspect. + +**What it is.** A child wrapper holds a **raw** pointer into a parent's native object but +keeps no managed reference to the parent. Finalization order is non-deterministic, so the +parent can be collected/finalized while the child is still alive. + +**Why it's bad.** The child then dereferences freed parent memory → **use-after-free**. +The fix is cheap (root the parent) and has zero ABI impact. + +**Leak (❌):** a child cursor is built from a parent's *raw* native handle but drops the +managed parent, so nothing keeps it alive: +```csharp +public class ChildCursor : SKObject, ISKSkipObjectRegistration +{ + internal ChildCursor(SKParent parent) + : base(SkiaApi.sk_parent_cursor_new(parent.Handle), owns: true) + { + // ❌ `parent` is discarded; the native cursor still points at parent's SkParent*. + } +} +``` + +**Fix (✓):** root the parent in a managed field for the child's whole lifetime: +```csharp +public class ChildCursor : SKObject, ISKSkipObjectRegistration +{ + private readonly SKParent parent; // keep the parent alive + internal ChildCursor(SKParent parent) + : base(SkiaApi.sk_parent_cursor_new(parent.Handle), owns: true) + { + this.parent = parent; + } +} +``` +For a one-shot P/Invoke (no stored child), `GC.KeepAlive(parent)` after the call is enough. + +**How to find it.** Look for wrapper types constructed from a parent's `.Handle` that keep +**no** field referencing that parent — especially when *sibling* wrappers of the same parent +type DO keep such a field (e.g. one iterator/cursor stores `private readonly SKParent` and +another doesn't). The odd one out is the prime suspect. Prove it before believing it (Phase 2). + +**Watch out (❌ don't):** don't root the parent with a pinned `GCHandle` — a plain managed +field is enough and a pinned handle is its own leak (family 9). And don't lean on +`GC.KeepAlive` for a *long-lived* child (an iterator you hold across calls); KeepAlive only +covers the current method, so a stored child needs the field. + +**Real cases:** #3796 (SKPath/SKPathBuilder finalizer race), #3291 (SKAutoCanvasRestore). + +--- + +## 6 — Clone / copy double-free + +**Where to look.** `binding/SkiaSharp/**`. `grep -rnE "Clone|MemberwiseClone|_clone" binding/SkiaSharp` — check whether the copy shares or duplicates the native pointer. + +**What it is.** A `Clone()`/copy that **shares** one native pointer between two managed +wrappers, both of which believe they own it and will dispose it. + +**Why it's bad.** Both wrappers call `DisposeNative` on the same handle → **double-free**. + +**Leak (❌):** +```csharp +public SKThing Clone() => + new SKThing(Handle, owns: true); // ❌ two wrappers own the same native object +``` + +**Fix (✓):** mint a *fresh* native object via the clone API: +```csharp +public SKThing Clone() => + GetObject(SkiaApi.sk_thing_clone(Handle)); // fresh handle, owns:true +``` + +**Watch out (❌ don't):** don't "fix" the double-free by setting `owns:false` on the clone — +that just swaps a double-free for a leak (or a use-after-free if the original is disposed +first). The clone must own a *separate* native object, not borrow the source's. + +**Real cases:** #2904 (SKPaint.Clone), #2899. + +--- + +## 7 — Disposing native statics / singletons + +**Where to look.** `binding/SkiaSharp/**`. `grep -rnE "GetDisposeProtectedObject|unrefExisting|CreateSrgb|Empty" binding/SkiaSharp` — immortal objects reached via a non-protected cache. + +**What it is.** An *immortal* native object — the default/empty typeface, the sRGB / +sRGB-linear color spaces and gamma color filters, the blend-mode blender cache, `SKData.Empty` +— reached through an accessor that is **not** dispose-protected, so the wrapper's +`DisposeNative` unrefs or deletes an object that must live for the whole process. + +**Why it's bad.** Unref'ing / deleting a process-wide singleton corrupts it for **every** +caller — crashes or wrong rendering far from the disposal site. + +**Leak/crash (❌):** +```csharp +public static SKColorSpace CreateSrgb() => + GetObject(SkiaApi.sk_colorspace_new_srgb(), owns: true); // ❌ singleton +``` + +**Fix (✓):** route through the dispose-protected accessor so `DisposeNative` is skipped: +```csharp +public static SKColorSpace CreateSrgb() => + GetDisposeProtectedObject( + SkiaApi.sk_colorspace_new_srgb(), owns: false, unrefExisting: false); +``` + +**Watch out (❌ don't):** don't null out or replace the cached static, and don't wrap it +`owns:true` "just in case." The only correct fix is the dispose-protected accessor with +`unrefExisting:false`; copy an existing correct singleton (`SKBlender` cache) rather than +inventing a new disposal path. + +**Real cases:** #1863, #4080, #1224, #3730. The `SKBlender` mode cache and `SKColorFilter` +gamma filters are the canonical correct implementations to copy. + +--- + +## 8 — Field not nulled on dispose + +**Where to look.** `binding/SkiaSharp/**`. `grep -rnE "DisposeManaged|= null;" binding/SkiaSharp` — a freed native child field that isn't cleared afterwards. + +**What it is.** A `Dispose`/`DisposeManaged` frees a cached native child (a canvas, a +sub-object) but leaves the managed field still pointing at the now-dead wrapper. + +**Why it's bad.** A later `Dispose` (or a caller re-reading the field) hits the freed object → +**double-dispose / `AccessViolationException`**; or the stale reference keeps a whole native +graph rooted → leak. + +**Leak (❌):** +```csharp +protected override void DisposeManaged() +{ + _canvas?.Dispose(); + // ❌ _canvas still references the disposed wrapper; a second Dispose double-frees. + base.DisposeManaged(); +} +``` + +**Fix (✓):** +```csharp +protected override void DisposeManaged() +{ + _canvas?.Dispose(); + _canvas = null; // clear the link → second dispose is a no-op + base.DisposeManaged(); +} +``` + +**Watch out (❌ don't):** don't null the field *before* disposing the child — you'd drop the +only reference and leak the native object instead. The order is fixed: **dispose, then null.** + +**Real cases:** #1256, #1344. `SKSurface` (nulls its cached `SKCanvas`) and `SKPixmap` (nulls +`pixelSource`) are the correct patterns. + +--- + +## 9 — Managed stream / callback / delegate-proxy lifetime + +**Where to look.** `binding/SkiaSharp/**`. `grep -rnE "DelegateProxies|GCHandle|ManagedStream|ReleaseDelegate" binding/SkiaSharp` — a `GCHandle`/proxy freed too early (dangling) or never (leak). + +**What it is.** A managed object handed to native code as a callback sink — an +`SKManagedStream`/`SKManagedWStream`/`SKAbstractManagedStream`, a delegate or function-pointer +proxy, or a `GCHandle` pinned for a release/destroy callback — is freed at the wrong time. + +**Why it's bad.** Freed **too early** → native invokes a delegate/`GCHandle` that's already +gone → **crash**. Freed **never** → the `GCHandle` and everything it roots leak for the +process lifetime. + +**Leak (❌):** +```csharp +// Allocate a GCHandle for the release proc, but never wire the destroy proxy that frees it. +DelegateProxies.Create(releaseProc, out _, out var ctx); +return HarfBuzzApi.hb_blob_create(ptr, len, mode, (void*)ctx, null); // ❌ proxy = null → ctx leaks +``` + +**Fix (✓):** pass the destroy proxy so native frees the `GCHandle` when it's done: +```csharp +DelegateProxies.Create(releaseProc, out _, out var ctx); +var proxy = releaseProc != null ? DelegateProxies.DestroyProxy : null; +return HarfBuzzApi.hb_blob_create(ptr, len, mode, (void*)ctx, proxy); +``` +Keep the handle rooted for **exactly** the native object's lifetime — not shorter, not longer. + +**Watch out (❌ don't):** don't `Free()` the `GCHandle` in the same method that hands it to +native — native still holds it. And don't leave the destroy proxy `null` to "avoid a crash"; +that leaks. Free it in the destroy/release callback, and only there. + +**Real cases:** #3589, #2916, #996, #2446. `SKManagedStream`/`DelegateProxies` use a `Weak` +user-data `GCHandle` freed by the destroy proxy — the reference implementation. + +--- + +## 10 — Allocation-failure path + +**Where to look.** `binding/SkiaSharp/**`. `grep -rnE "GetObject\(\s*[a-z]|if \(handle == " binding/SkiaSharp` — a wrapper returned (or half-built) even when the native create returned null. + +**What it is.** A factory wraps and returns a managed object even when the native +create/decode returned `null`/`0` or failed, or leaks a half-built native object on the error +path. + +**Why it's bad.** A wrapper around `IntPtr.Zero` throws `NullReferenceException` / +`AccessViolationException` on first use, far from the real failure. A half-built native left +un-freed on the error branch is a straight leak. + +**Leak/crash (❌):** +```csharp +public static SKFoo Create(...) +{ + var handle = SkiaApi.sk_foo_new(...); // may return IntPtr.Zero on failure + return new SKFoo(handle, owns: true); // ❌ wraps a null handle +} +``` + +**Fix (✓):** +```csharp +public static SKFoo? Create(...) +{ + var handle = SkiaApi.sk_foo_new(...); + if (handle == IntPtr.Zero) + return null; // factory returns null on failure + return new SKFoo(handle, owns: true); +} +``` +On multi-step builds, free any partial native objects before returning on the error path. + +**Watch out (❌ don't):** don't make a *factory* throw when its contract is to return `null` +(that's an ABI/behavior break — add the null-return, don't change the exception surface). +And don't return `null` while leaving an earlier half-built native object un-freed on the +error branch. + +**Real cases:** #1784, #1642. `SKCodec.Create` (revokes stream ownership before disposing on +`codec == null`) and `SKColorSpaceIccProfile.Create` (disposes the half-built profile on parse +failure) are the correct patterns. diff --git a/.github/workflows/memory-leak-fixer.lock.yml b/.github/workflows/memory-leak-fixer.lock.yml new file mode 100644 index 00000000000..f2c9b662200 --- /dev/null +++ b/.github/workflows/memory-leak-fixer.lock.yml @@ -0,0 +1,1514 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"530ccc128b53a8a7cac58035d8646b3b03c74f0b61624221816a7f96b35dedcf","compiler_version":"v0.71.5","strict":true,"agent_id":"copilot","agent_model":"claude-opus-4.8"} +# 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":"b8068426813005612b960b5ab0b8bd2c27142323","version":"v0.71.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.40","digest":"sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40","digest":"sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.40","digest":"sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.71.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/ +# +# Scan the repo's managed C# for a native ownership/disposal memory leak, prove it with a red→green test, fix it, and open a draft PR. +# +# 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/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 +# - ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c +# - ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 +# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + +name: "Fixer - Memory Leak" +"on": + pull_request: + paths: + - .github/workflows/memory-leak-fixer.md + - .github/workflows/memory-leak-fixer.lock.yml + - .agents/skills/memory-leak-fixer/** + schedule: + - cron: "13 */12 * * *" + # Friendly format: every 12h (scattered) + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + dry_run: + default: false + description: Do the full scan→prove→fix locally but do NOT open a PR/issue. + required: false + type: boolean + +permissions: {} + +concurrency: + cancel-in-progress: false + group: memory-leak-fixer + +run-name: "Fixer - Memory Leak" + +jobs: + activation: + needs: pre_activation + if: > + needs.pre_activation.outputs.activated == 'true' && ((github.repository == 'mono/SkiaSharp') && (github.event_name != 'pull_request' || + github.event.pull_request.head.repo.id == github.repository_id)) + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + body: ${{ steps.sanitized.outputs.body }} + 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 }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + text: ${{ steps.sanitized.outputs.text }} + title: ${{ steps.sanitized.outputs.title }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Fixer - Memory Leak" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/memory-leak-fixer.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - 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: "claude-opus-4.8" + GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_AGENT_VERSION: "1.0.40" + GH_AW_INFO_CLI_VERSION: "v0.71.5" + GH_AW_INFO_WORKFLOW_NAME: "Fixer - Memory Leak" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","github","dotnet","*.blob.core.windows.net"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.40" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + 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: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .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 .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.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: "memory-leak-fixer.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.71.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: Compute current body text + id: sanitized + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_ALLOWED_DOMAINS: "*.blob.core.windows.net,*.githubusercontent.com,*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,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,dc.services.visualstudio.com,dist.nuget.org,docs.github.com,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,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,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,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,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.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,www.microsoft.com" + 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/compute_text.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_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_INPUTS_DRY_RUN: ${{ github.event.inputs.dry_run }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_NAME: ${{ github.event_name }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + 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_f3b4c6dfdadc92b1_EOF' + + GH_AW_PROMPT_f3b4c6dfdadc92b1_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_f3b4c6dfdadc92b1_EOF' + + Tools: create_issue, create_pull_request, missing_tool, missing_data, noop + GH_AW_PROMPT_f3b4c6dfdadc92b1_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat << 'GH_AW_PROMPT_f3b4c6dfdadc92b1_EOF' + + GH_AW_PROMPT_f3b4c6dfdadc92b1_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_f3b4c6dfdadc92b1_EOF' + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + - **checkouts**: The following repositories have been checked out and are available in the workspace: + - `$GITHUB_WORKSPACE` → `__GH_AW_GITHUB_REPOSITORY__` (cwd) [full history, all branches available as remote-tracking refs] + - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). + + + GH_AW_PROMPT_f3b4c6dfdadc92b1_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_f3b4c6dfdadc92b1_EOF' + + {{#runtime-import .github/workflows/memory-leak-fixer.md}} + GH_AW_PROMPT_f3b4c6dfdadc92b1_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" + GH_AW_GITHUB_EVENT_INPUTS_DRY_RUN: ${{ github.event.inputs.dry_run }} + GH_AW_GITHUB_EVENT_NAME: ${{ github.event_name }} + 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_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_INPUTS_DRY_RUN: ${{ github.event.inputs.dry_run }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_NAME: ${{ github.event_name }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + 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_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_INPUTS_DRY_RUN: process.env.GH_AW_GITHUB_EVENT_INPUTS_DRY_RUN, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_NAME: process.env.GH_AW_GITHUB_EVENT_NAME, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + 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/aw-prompts/prompt.txt + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + environment: gh-aw-agents + permissions: + contents: read + issues: read + pull-requests: read + 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: memoryleakfixer + outputs: + agentic_engine_timeout: ${{ steps.detect-copilot-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 }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-copilot-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-copilot-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-copilot-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Fixer - Memory Leak" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/memory-leak-fixer.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - 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 + fetch-depth: 0 + - 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: Redirect step summary into agent-writable directory + run: |- + mkdir -p /tmp/gh-aw/agent + touch /tmp/gh-aw/agent/step-summary.md + rm -f /tmp/gh-aw/agent-step-summary.md + ln -s /tmp/gh-aw/agent/step-summary.md /tmp/gh-aw/agent-step-summary.md + + - 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.40 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.40 + - name: Parse integrity filter lists + id: parse-guard-vars + env: + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" + - 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 .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.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.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + - 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_75e3faa6d0f7456d_EOF' + {"create_issue":{"allowed_labels":["agentic-workflows"],"labels":["agentic-workflows"],"max":1,"title_prefix":"[memory-leak] "},"create_pull_request":{"allowed_base_branches":["main"],"draft":true,"labels":["agentic-workflows"],"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"],"title_prefix":"[memory-leak] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_75e3faa6d0f7456d_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[memory-leak] \". Labels [\"agentic-workflows\"] will be automatically added. Only these labels are allowed: [\"agentic-workflows\"].", + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[memory-leak] \". Labels [\"agentic-workflows\"] will be automatically added. PRs will be created as drafts." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "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 }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + 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') + DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 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 /var/run/docker.sock:/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 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.6' + + 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_37b454bef4d3683b_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v1.0.3", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "issues,pull_requests,search" + }, + "guard-policies": { + "allow-only": { + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": [ + "mono/skiasharp" + ], + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} + } + } + }, + "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": [ + "private:mono/skiasharp" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_37b454bef4d3683b_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: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool shell(awk) + # --allow-tool shell(basename) + # --allow-tool shell(bash) + # --allow-tool shell(cat) + # --allow-tool shell(chmod) + # --allow-tool shell(cp) + # --allow-tool shell(cut) + # --allow-tool shell(date) + # --allow-tool shell(dirname) + # --allow-tool shell(dotnet:*) + # --allow-tool shell(echo) + # --allow-tool shell(env) + # --allow-tool shell(find) + # --allow-tool shell(gh:*) + # --allow-tool shell(git add:*) + # --allow-tool shell(git branch:*) + # --allow-tool shell(git checkout:*) + # --allow-tool shell(git commit:*) + # --allow-tool shell(git merge:*) + # --allow-tool shell(git rm:*) + # --allow-tool shell(git status) + # --allow-tool shell(git switch:*) + # --allow-tool shell(git:*) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(mkdir) + # --allow-tool shell(mv) + # --allow-tool shell(pwd) + # --allow-tool shell(pwsh) + # --allow-tool shell(rm) + # --allow-tool shell(safeoutputs:*) + # --allow-tool shell(sed) + # --allow-tool shell(sh) + # --allow-tool shell(sort) + # --allow-tool shell(tail) + # --allow-tool shell(tee) + # --allow-tool shell(test) + # --allow-tool shell(tr) + # --allow-tool shell(uniq) + # --allow-tool shell(wc) + # --allow-tool shell(xargs) + # --allow-tool shell(yq) + # --allow-tool write + timeout-minutes: 120 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.40/awf-config.schema.json","network":{"allowDomains":["*.blob.core.windows.net","*.githubusercontent.com","*.vsblob.vsassets.io","api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.nuget.org","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","azuresearch-usnc.nuget.org","azuresearch-ussc.nuget.org","builds.dotnet.microsoft.com","ci.dot.net","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","dc.services.visualstudio.com","dist.nuget.org","docs.github.com","dot.net","dotnet.microsoft.com","dotnetcli.blob.core.windows.net","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","nuget.org","nuget.pkg.github.com","nugetregistryv2prod.blob.core.windows.net","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","oneocsp.microsoft.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","pkgs.dev.azure.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","www.microsoft.com"]},"apiProxy":{"enabled":true,"models":{"auto":["large"],"deep-research":["copilot/deep-research*","google/deep-research*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"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*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash"],"opus":["copilot/*opus*","anthropic/*opus*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"]}},"container":{"imageTag":"0.25.40,squid=sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51,agent=sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504,api-proxy=sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280,cli-proxy=sha256:3e7152911d4b4b7b97beef9d3d7d924ff7902227e86001ef3838fb728d5d514c"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + # 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" --env-all --exclude-env COPILOT_GITHUB_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 \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -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 || echo node)"; 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-tool github --allow-tool safeoutputs --allow-tool '\''shell(awk)'\'' --allow-tool '\''shell(basename)'\'' --allow-tool '\''shell(bash)'\'' --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(chmod)'\'' --allow-tool '\''shell(cp)'\'' --allow-tool '\''shell(cut)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(dirname)'\'' --allow-tool '\''shell(dotnet:*)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(env)'\'' --allow-tool '\''shell(find)'\'' --allow-tool '\''shell(gh:*)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(git:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(mkdir)'\'' --allow-tool '\''shell(mv)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(pwsh)'\'' --allow-tool '\''shell(rm)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(sed)'\'' --allow-tool '\''shell(sh)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(tee)'\'' --allow-tool '\''shell(test)'\'' --allow-tool '\''shell(tr)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(xargs)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --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: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: claude-opus-4.8 + 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.71.5 + 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] + XDG_CONFIG_HOME: /home/runner + - name: Detect Copilot errors + id: detect-copilot-errors + if: always() + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_copilot_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: "*.blob.core.windows.net,*.githubusercontent.com,*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,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,dc.services.visualstudio.com,dist.nuget.org,docs.github.com,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,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,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,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,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.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,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+r /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/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ + /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 + environment: gh-aw-agents + permissions: + contents: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-memory-leak-fixer" + cancel-in-progress: false + 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@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Fixer - Memory Leak" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/memory-leak-fixer.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - 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: "Fixer - Memory Leak" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + 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: "Fixer - Memory Leak" + 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: "Fixer - Memory Leak" + 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: "Fixer - Memory Leak" + 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: "Fixer - Memory Leak" + 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: "memory-leak-fixer" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + 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: "120" + 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@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Fixer - Memory Leak" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/memory-leak-fixer.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - 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.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 + - 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 + 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: "Fixer - Memory Leak" + WORKFLOW_DESCRIPTION: "Scan the repo's managed C# for a native ownership/disposal memory leak, prove it with a red→green test, fix it, and open a draft PR." + 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.40 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.40 + - 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 + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + (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.40/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","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.40,squid=sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51,agent=sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504,api-proxy=sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280,cli-proxy=sha256:3e7152911d4b4b7b97beef9d3d7d924ff7902227e86001ef3838fb728d5d514c"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + # 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" --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 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -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 || echo node)"; 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: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: claude-opus-4.8 + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.71.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] + 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 }} + 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 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) { + 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: + if: > + (github.repository == 'mono/SkiaSharp') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id) + runs-on: ubuntu-slim + environment: gh-aw-agents + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + matched_command: '' + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Fixer - Memory Leak" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/memory-leak-fixer.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - 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(); + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + environment: gh-aw-agents + permissions: + contents: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/memory-leak-fixer" + 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: "claude-opus-4.8" + GH_AW_ENGINE_VERSION: "1.0.40" + GH_AW_WORKFLOW_ID: "memory-leak-fixer" + GH_AW_WORKFLOW_NAME: "Fixer - Memory Leak" + 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_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} + 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@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Fixer - Memory Leak" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/memory-leak-fixer.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - 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: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ 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_ALLOWED_DOMAINS: "*.blob.core.windows.net,*.githubusercontent.com,*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,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,dc.services.visualstudio.com,dist.nuget.org,docs.github.com,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,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,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,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,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.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,www.microsoft.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"allowed_labels\":[\"agentic-workflows\"],\"labels\":[\"agentic-workflows\"],\"max\":1,\"title_prefix\":\"[memory-leak] \"},\"create_pull_request\":{\"allowed_base_branches\":[\"main\"],\"draft\":true,\"labels\":[\"agentic-workflows\"],\"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\"],\"title_prefix\":\"[memory-leak] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"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/memory-leak-fixer.md b/.github/workflows/memory-leak-fixer.md new file mode 100644 index 00000000000..68e83ef3ce9 --- /dev/null +++ b/.github/workflows/memory-leak-fixer.md @@ -0,0 +1,208 @@ +--- +# ============================================================================= +# Fixer - Memory Leak +# +# Periodic AI-driven workflow that SCANS the repo's managed C# for a native +# ownership / disposal memory leak, PROVES it with a red→green regression test, +# FIXES it, and opens a DRAFT PR. Adapted from dotnet/maui's leak scanner+fixer +# idea to our real leak family (undisposed SKObject handles, wrong +# `owns:` flags, same-instance double-dispose, unremoved view/handler +# subscriptions, `fixed`-pointer lifetime). Scope: managed C# only — the native +# Skia under externals/skia/** is upstream and out of scope. +# +# All the domain knowledge lives in the reusable skill +# `.agents/skills/memory-leak-fixer/SKILL.md`; this file is the schedule + the +# guardrails (one finding per run, de-dup, validate-before-PR). +# ============================================================================= +description: "Scan the repo's managed C# for a native ownership/disposal memory leak, prove it with a red→green test, fix it, and open a draft PR." + +environment: gh-aw-agents + +# -- Engine ------------------------------------------------------------ +# Leak reasoning (ownership tracing + a correct minimal fix) is hard and +# benefits from the stronger model, matching auto-skia-sync's choice. +engine: + id: copilot + model: claude-opus-4.8 + +# -- Triggers ---------------------------------------------------------- +# Every 12h + manual + PR-driven self-test. Every run does the same full +# scan→prove→fix→file pipeline; the only knob is `dry_run` (do everything but +# open nothing). A `pull_request` that edits THIS workflow or its skill re-runs +# the whole pipeline in FORCED DRY-RUN (see Step 2.6) so we can iterate on the +# prompt/skill and watch the run without ever opening a real PR/issue. +on: + schedule: every 12h + workflow_dispatch: + inputs: + dry_run: + description: "Do the full scan→prove→fix locally but do NOT open a PR/issue." + required: false + default: false + type: boolean + pull_request: + paths: + - ".github/workflows/memory-leak-fixer.md" + - ".github/workflows/memory-leak-fixer.lock.yml" + - ".agents/skills/memory-leak-fixer/**" + +# Only ever run on the canonical repo — never on forks. +if: | + github.repository == 'mono/SkiaSharp' + +# Give the skill's Phase 5 report a writable step-summary sink (mirrors the +# convention used by auto-triage). +steps: + - name: Redirect step summary into agent-writable directory + run: | + mkdir -p /tmp/gh-aw/agent + touch /tmp/gh-aw/agent/step-summary.md + rm -f /tmp/gh-aw/agent-step-summary.md + ln -s /tmp/gh-aw/agent/step-summary.md /tmp/gh-aw/agent-step-summary.md + +concurrency: + group: "memory-leak-fixer" + cancel-in-progress: false + +timeout-minutes: 120 + +# create-pull-request is commit-based and needs full history for its branch ops +# (and for the skill's git-commit-count focus rotation). This skill is managed-C# +# only and builds against pre-built natives (externals-download), so the skia +# source submodule is NOT needed — a plain checkout keeps runs fast. +checkout: + - fetch-depth: 0 + +permissions: + contents: read + issues: read + pull-requests: read + +# -- Agent tools ----------------------------------------------------- +# The agent reads source, writes a throwaway probe + a regression test, edits +# managed C# to fix, builds, tests, then commits. `dotnet`/`pwsh`/`git` are the +# work tools; `gh` powers de-dup queries. No `sed`/`awk` in-place edits — use the +# edit tool so changes are reviewable. +tools: + github: + toolsets: [issues, pull_requests, search] + allowed-repos: ["mono/skiasharp"] + min-integrity: none + edit: + bash: ["dotnet", "pwsh", "git", "gh", "find", "ls", "cat", "grep", "head", "tail", "wc", "jq", "tee", "sed", "awk", "tr", "cut", "sort", "uniq", "xargs", "echo", "date", "mkdir", "test", "env", "basename", "dirname", "bash", "sh", "chmod", "cp", "mv", "rm"] + +# -- Network allowlist ----------------------------------------------- +# nuget.org for the shipped-package leak probe; the SkiaSharp-CI Azure DevOps +# feed + blob storage for `externals-download` pre-built natives; the `dotnet` +# ecosystem covers pkgs.dev.azure.com and the SDK/restore hosts. +network: + allowed: + - defaults + - github + - dotnet + - "*.blob.core.windows.net" + +# -- Safe outputs ------------------------------------------------------ +# A confirmed, managed-C#-fixable leak emits a PAIR: a [memory-leak] issue (the +# finding) + a draft fix PR that closes it on merge (Fixes #, resolved +# by gh-aw to the real number). When the only correct fix is native / upstream +# Skia (out of scope here), the issue is filed alone. Quiet/dry runs emit a noop. +safe-outputs: + create-pull-request: + title-prefix: "[memory-leak] " + labels: [agentic-workflows] + draft: true + allowed-base-branches: [main] + create-issue: + title-prefix: "[memory-leak] " + labels: [agentic-workflows] + allowed-labels: [agentic-workflows] + max: 1 + noop: + report-as-issue: false +--- + +# Fixer - Memory Leak + +You **scan** the repo's **managed C#** for one native ownership / disposal memory leak, +**prove** it with a red→green regression test, **fix** it, then **file the finding as a +`[memory-leak]` issue and open a draft PR that closes it** (`Fixes #…`) — or, when the only +correct fix is in native / upstream Skia (out of scope here), file the `[memory-leak]` issue +alone. **One finding per run.** + +**Read this first — set your expectations correctly:** + +- This codebase is **mature and heavily-hardened**. There is **no planted/seeded/injected + bug** to find. Most runs will (and should) find **nothing convincing** — that is the normal, + expected, successful outcome, **not** a failure and **not** a puzzle with a guaranteed answer. + Do **not** invent a leak, do **not** rationalize well-documented, deliberately-hardened code + as "decoys," and do **not** lower your evidence bar just to produce a result. +- A quiet run is a **first-class success**: when you find nothing that clears the bar, emit + exactly **one `noop`** safe output summarizing what you scanned and why nothing qualified. + A `noop` is the correct "nothing to do" signal — silence makes the run look incomplete. +- **Scope is managed C# only.** Fix only code the repo owns — `binding/**` and `source/**`. + Everything under `externals/skia/**` (including our C shim) is upstream Skia: not checked + out, not buildable here (native tests use pre-built packages via `externals-download`), and + out of scope. A leak whose only correct fix is native is **issue-only** (see 2.4). +- **Timebox the scan.** Do one focused pass over the leak families, pick the single strongest + candidate early, and stop. If nothing clears the bar in that pass, emit the `noop` and finish + — do not launch open-ended sub-agent explorations that may not return within the budget. + +All the methodology lives in the skill. Follow it exactly. + +## Step 1 — Run the memory-leak-fixer skill + +Read and follow `.agents/skills/memory-leak-fixer/SKILL.md` end-to-end. + +**Every run does the same thing — "Scan and fix"** (skill Phase 1 → 2 → 3 → 4 → 5): hunt a new +leak, prove it, fix it, then file the finding issue + linked fix PR. There is no per-issue target +mode. The only variation is dry-run — forced on `pull_request`, opt-in via the `dry_run` input +(see Guardrail 6). + +Persist all intermediate state (the `/tmp/leakprobe` project, notes) under `/tmp/gh-aw/agent/`. +Each bash call is a fresh subshell — re-`cd` as needed. + +## Step 2 — Guardrails (in addition to the skill's golden rules) + +1. **What to emit — one finding per run.** For a confirmed, managed-C#-fixable leak, emit the + **linked pair**: one `create-issue` (the finding, with a `temporary_id`) **and** one + `create-pull-request` (the fix, whose body ends with `Fixes #` so merging + auto-closes the issue) — see skill Phase 4. If the only correct fix is native / upstream (out + of scope) → emit the **`create-issue` alone**. If nothing clears the bar → exactly one + **`noop`**. Never finish a run with no safe output at all (that makes the run look incomplete). +2. **De-dup first.** Run skill Phase 1.3 — skip any candidate already covered by an OPEN + `[memory-leak]` issue or PR on `mono/SkiaSharp`. A candidate whose only prior item is + CLOSED may be re-filed. +3. **Validate before you open a PR.** Only open a PR when you have demonstrated the + regression test **fails without the fix and passes with it** (skill Phase 3, both + directions). No red→green ⇒ no PR. +4. **Managed-C# fixes only.** The fix must live in `binding/**` / `source/**`, bootstrapped + with `dotnet cake --target=externals-download` (pre-built natives) and validated with + `dotnet test`. If the strongest candidate's only correct fix is in native / upstream Skia + (`externals/skia/**`, including the C shim), do **not** open an unvalidated PR — file a + `[memory-leak]` issue with the Phase 1–2 evidence and the proposed fix instead. +5. **Never weaken, skip, mute, or delete a test, and never edit `*.generated.cs` or anything + under `externals/skia/**`** (upstream Skia + the C shim are out of scope for this skill). +6. **Dry run (forced on PRs).** Decide from the trigger `${{ github.event_name }}`: + `pull_request` → **always DRY-RUN** (a PR editing this workflow/skill is a self-test); + `workflow_dispatch` with the **dry_run** input `true` (shown as + `${{ github.event.inputs.dry_run }}`) → **DRY-RUN**; `schedule` → real run. In DRY-RUN you do the + full scan→prove→fix locally but you **MUST NOT** emit any `create-pull-request` or + `create-issue` safe output under any circumstances. Instead, report your findings in the + step summary **and** emit exactly one **`noop`** whose body is that same summary (what you + scanned, the strongest candidate if any, and — had this been a real run — the finding you + would have filed and the fix PR you would have opened). Never finish a dry run with no safe output. +7. **AI attribution.** Every PR/issue body must clearly state it was produced by this + agentic workflow + the `memory-leak-fixer` skill, and include an honest scope note + (framework bug vs footgun; empirically-proven vs statically-reasoned; ABI impact). + +## Step 3 — Report + +Append a short summary to `/tmp/gh-aw/agent/step-summary.md` (this file is symlinked to the +run's step summary — do **not** use `$GITHUB_STEP_SUMMARY`): the leak family, the candidate +(with `file:line`), the proof result (alive/collected counts or red→green status), and the +resulting issue + PR links — or "no convincing candidate this run" for a quiet run. + +Then make sure you have emitted the safe output(s) from Step 2.1: the **issue + PR pair** (or a +`create-issue` alone when the fix is out of scope), or — for a dry run or a quiet run — a single +`noop` carrying this same summary. Never finish the run with no safe output. diff --git a/AGENTS.md b/AGENTS.md index 05f5c3a5b91..6c1eb1e5735 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -349,6 +349,7 @@ Custom slash commands are available for specialized workflows. Use these for com | Triage issue | `/issue-triage` | "triage #NNNN", "classify issue", "analyze issue" | | Reproduce bug | `/issue-repro` | "repro #NNNN", "reproduce issue", "create reproduction" | | Fix bug | `/issue-fix` | "investigate #NNNN", "fix issue", crash, exception, segfault, "doesn't work" | +| Scan/fix memory leak | `/memory-leak-fixer` | "memory leak", "leak scan", "undisposed handle", "owns flag", "double free", "fix the leak" | | Bulk process issues | `/issue-bulk-process` | "triage these issues", "process issues #1 #2 #3" | | Add new API | `/api-add-review` | "expose", "wrap method", issue requests new functionality | | Update dependency | `/native-dependency-update` | "bump libpng", "fix CVE in zlib" | @@ -386,6 +387,7 @@ See [documentation/dev/issue-pipeline.md](documentation/dev/issue-pipeline.md) f | "triage", "classify", "analyze issue" | Triage | `/issue-triage` | | "repro", "reproduce", "reproduction" | Reproduction | `/issue-repro` | | "crash", "exception", "wrong", "fails", "broken", "segfault" | Bug | `/issue-fix` | +| "memory leak", "not disposed", "handle leak", "owns flag", "double free" | Memory leak | `/memory-leak-fixer` | | "add", "expose", "missing API", "feature request" | New API | `/api-add-review` | | "docs", "documentation", "XML", "comments" | Docs | `/api-docs` | | CVE, security, vulnerability | Security | `/security-audit` then `/native-dependency-update` |