Repository: docker/buildx

Issue:
Debugger does not pause when copying a non-existent file
### Contributing guidelines

- [x] I've read the [contributing guidelines](https://github.com/docker/buildx/blob/master/.github/CONTRIBUTING.md) and wholeheartedly agree

### I've found a bug and checked that ...

- [x] ... the documentation does not mention anything about my problem
- [x] ... there are no open or closed issues that are related to my problem

### Description

When trying to debug a Dockerfile build that uses `COPY` on something that does not exist in the build context, the debug session is terminated instead of pausing. In comparison, if I do `RUN cat non-existent-file.txt` the debugger _does_ pause so I would have expected the `COPY` call to also suspend the debug session instead of simply terminating.

### Expected behaviour

The debug session should suspend instead of terminating.

### Actual behaviour

The debug session terminates.

### Buildx version

github.com/docker/buildx v0.29.0-rc1-2-gb4b698c0 b4b698c088b1bcfe2f47ca6fda9883064a2bc28f

### Docker info

```text
Client:
 Version:    28.4.0
 Context:    desktop-linux
 Debug Mode: false
 Plugins:
  ai: Docker AI Agent - Ask Gordon (Docker Inc.)
    Version:  v1.9.11
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-ai
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.29.0-rc1-2-gb4b698c0
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-buildx
  cloud: Docker Cloud (Docker Inc.)
    Version:  v0.4.27
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-cloud
  compose: Docker Compose (Docker Inc.)
    Version:  v2.39.4-desktop.1
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-compose
  debug: Get a shell into any image or container (Docker Inc.)
    Version:  0.0.42
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-debug
  desktop: Docker Desktop commands (Docker Inc.)
    Version:  v0.2.0
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-desktop
  extension: Manages Docker extensions (Docker Inc.)
    Version:  v0.2.31
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-extension
  init: Creates Docker-related starter files for your project (Docker Inc.)
    Version:  v1.4.0
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-init
  mcp: Docker MCP Plugin (Docker Inc.)
    Version:  v0.20.0
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-mcp
  model: Docker Model Runner (Docker Inc.)
    Version:  v0.1.40
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-model
  offload: Docker Offload (Docker Inc.)
    Version:  v0.4.27
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-offload
  sbom: View the packaged-based Software Bill Of Materials (SBOM) for an image (Anchore Inc.)
    Version:  0.6.0
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-sbom
  scout: Docker Scout (Docker Inc.)
    Version:  v1.18.3
    Path:     /Users/rcjsuen/.docker/cli-plugins/docker-scout

Server:
 Containers: 2
  Running: 0
  Paused: 0
  Stopped: 2
 Images: 39
 Server Version: 28.4.0
 Storage Driver: overlayfs
  driver-type: io.containerd.snapshotter.v1
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 CDI spec directories:
  /etc/cdi
  /var/run/cdi
 Discovered Devices:
  cdi: docker.com/gpu=webgpu
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 05044ec0a9a75232cad458027ca83437aae3f4da
 runc version: v1.2.5-0-g59923ef
 init version: de40ad0
 Security Options:
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 6.10.14-linuxkit
 Operating System: Docker Desktop
 OSType: linux
 Architecture: aarch64
 CPUs: 14
 Total Memory: 7.653GiB
 Name: docker-desktop
 ID: bdffc6bd-ef47-45bc-9cbc-9e2bd56bc69d
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 HTTP Proxy: http.docker.internal:3128
 HTTPS Proxy: http.docker.internal:3128
 No Proxy: hubproxy.docker.internal
 Labels:
  com.docker.desktop.address=unix:///Users/rcjsuen/Library/Containers/com.docker.docker/Data/docker-cli.sock
 Experimental: false
 Insecure Registries:
  hubproxy.docker.internal:5555
  ::1/128
  127.0.0.0/8
 Live Restore Enabled: false
```

### Builders list

```text
NAME/NODE           DRIVER/ENDPOINT     STATUS    BUILDKIT   PLATFORMS
default             docker
 \_ default          \_ default         running   v0.24.0    linux/amd64 (+2), linux/arm64, linux/ppc64le, linux/s390x, (2 more)
desktop-linux*      docker
 \_ desktop-linux    \_ desktop-linux   running   v0.24.0    linux/amd64 (+2), linux/arm64, linux/ppc64le, linux/s390x, (2 more)
```

### Configuration

```Dockerfile
FROM node:24-alpine AS stage
COPY non-existent-file.txt .
```

Debug this file with VS Code and it just terminates instead of suspending.

### Build logs

```text

```

### Additional info

_No response_

Discussion:
@jsternberg: Need to check if this gets resolved by https://github.com/docker/buildx/pull/3450 or can be resolved as part of it.

I suspect the reason for this is because of the failure mode. When we stop on failure, we rely on the code path for exec which only supports stopping on exec calls. It might be considering the failure to be a non-builder failure and just exiting. I'll check it out.@jsternberg: Diving into this quite a bit. The problem seems to be with the rewind function. What rewind tries to do is find the actual digest that failed by taking the op that was returned as part of the solve error, remarshaling it to find the digest, and finding that in the build graph.

It seems like we're seeing a different digest for this one. I did a hexdump of the two and it seems like the solve error returns a JSON blob that includes the platform but the original solve that we did returns a representation that doesn't include the platform.

Now I just need to figure out if it's a bug that we didn't get back the platform from the solve or if we need to filter the platform from the solve error to determine what's the correct way to go about this.

Basically: The solve error we use to find the error step that failed is returning more information than it expects. Not sure where the correct place to inject or remove that extra information needs to go. I suspect the data is supposed to be there though.

Solution patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9dffcb8e..8f7b0dfa 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -167,7 +167,7 @@ jobs:
       -
         name: Upload test reports
         if: always()
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v5
         with:
           name: test-reports-${{ env.TESTREPORTS_NAME }}
           path: ./bin/testreports
@@ -245,7 +245,7 @@ jobs:
       -
         name: Upload test reports
         if: always()
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v5
         with:
           name: test-reports-${{ env.TESTREPORTS_NAME }}
           path: ${{ env.TESTREPORTS_BASEDIR }}
@@ -401,7 +401,7 @@ jobs:
           CACHE_TO: type=gha,scope=binaries-${{ env.PLATFORM_PAIR }},mode=max
       -
         name: Upload artifacts
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v5
         with:
           name: buildx-${{ env.PLATFORM_PAIR }}
           path: ${{ env.DESTDIR }}/*
@@ -516,7 +516,7 @@ jobs:
         uses: actions/checkout@v5
       -
         name: Download binaries
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v6
         with:
           path: ${{ env.DESTDIR }}
           pattern: buildx-*
diff --git a/.github/workflows/docs-upstream.yml b/.github/workflows/docs-upstream.yml
index 72cb3d1f..2586d1cc 100644
--- a/.github/workflows/docs-upstream.yml
+++ b/.github/workflows/docs-upstream.yml
@@ -58,7 +58,7 @@ jobs:
           DOCS_FORMATS: yaml
       -
         name: Upload reference YAML docs
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v5
         with:
           name: docs-yaml
           path: /tmp/buildx-docs/out/reference
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index c0311fcd..f9db15a8 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -57,7 +57,7 @@ jobs:
           mv ${{ env.DESTDIR }}/build/buildx ${{ env.DESTDIR }}/build/docker-buildx
       -
         name: Upload artifacts
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v5
         with:
           name: binary
           path: ${{ env.DESTDIR }}/build
@@ -118,7 +118,7 @@ jobs:
         if: matrix.driver == 'docker' || matrix.driver == 'docker-container'
       -
         name: Install buildx
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v6
         with:
           name: binary
           path: /home/runner/.docker/cli-plugins
@@ -230,7 +230,7 @@ jobs:
         uses: docker/setup-qemu-action@v3
       -
         name: Install buildx
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v6
         with:
           name: binary
           path: /home/runner/.docker/cli-plugins
diff --git a/dap/adapter.go b/dap/adapter.go
index ade3139c..12e8bfae 100644
--- a/dap/adapter.go
+++ b/dap/adapter.go
@@ -568,6 +568,9 @@ func newBreakpointMap() *breakpointMap {
 func (b *breakpointMap) Set(fname string, sbps []dap.SourceBreakpoint) (breakpoints []dap.Breakpoint) {
 	b.mu.Lock()
 	defer b.mu.Unlock()
+	// explicitly initialize breakpoints so that
+	// we do not send a null back in the JSON if there are no breakpoints
+	breakpoints = []dap.Breakpoint{}
 
 	prev := b.byPath[fname]
 	for _, sbp := range sbps {
diff --git a/dap/thread.go b/dap/thread.go
index 42a65c8f..47efbe2b 100644
--- a/dap/thread.go
+++ b/dap/thread.go
@@ -15,6 +15,7 @@ import (
 	"github.com/moby/buildkit/solver/pb"
 	"github.com/opencontainers/go-digest"
 	"github.com/pkg/errors"
+	"golang.org/x/sync/errgroup"
 )
 
 type thread struct {
@@ -73,16 +74,16 @@ func (t *thread) Evaluate(ctx Context, c gateway.Client, headRef gateway.Referen
 	}
 
 	var (
-		ref    gateway.Reference
-		mounts map[string]gateway.Reference
-		next   = t.entrypoint
-		err    error
+		k    string
+		refs map[string]gateway.Reference
+		next = t.entrypoint
+		err  error
 	)
 	for next != nil {
 		event := t.needsDebug(next, action, err)
 		if event.Reason != "" {
 			select {
-			case action = <-t.pause(ctx, ref, err, mounts, next, event):
+			case action = <-t.pause(ctx, k, refs, err, next, event):
 				// do nothing here
 			case <-ctx.Done():
 				return context.Cause(ctx)
@@ -96,7 +97,7 @@ func (t *thread) Evaluate(ctx Context, c gateway.Client, headRef gateway.Referen
 		if action == stepContinue {
 			t.setBreakpoints(ctx)
 		}
-		ref, next, mounts, err = t.seekNext(ctx, next, action)
+		k, next, refs, err = t.seekNext(ctx, next, action)
 	}
 	return nil
 }
@@ -121,8 +122,8 @@ func (t *thread) init(ctx Context, c gateway.Client, ref gateway.Reference, meta
 }
 
 type step struct {
-	// dgst holds the digest that should be resolved by this step.
-	// If this is empty, no digest should be resolved.
+	// dgst holds the digest associated with this step. This is used for
+	// breakpoint resolution.
 	dgst digest.Digest
 
 	// in holds the next target when step in is used.
@@ -136,6 +137,9 @@ type step struct {
 
 	// frame will hold the stack frame associated with this step.
 	frame *frame
+
+	// parent holds the index of the parent step.
+	parent int
 }
 
 func (t *thread) createProgram() error {
@@ -143,71 +147,99 @@ func (t *thread) createProgram() error {
 
 	// Create the entrypoint by using the last node.
 	// We will build on top of that.
-	head := &step{
-		dgst:  t.head,
-		frame: t.getStackFrame(t.head, nil),
-	}
-	t.entrypoint = t.createBranch(head)
+	t.entrypoint = t.createBranch(t.head, nil)
 	return nil
 }
 
-func (t *thread) createBranch(last *step) (first *step) {
-	first = last
-	for first.dgst != "" {
-		prev := &step{
-			// set to first temporarily until we determine
-			// if there are other inputs.
-			in: first,
-			// always first
-			next: first,
-			// exit point always matches the one set on first
-			out: first.out,
-			// always set to the same as next which is always first
-			frame: t.getStackFrame(first.dgst, first),
+func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *step) {
+	// Construct the final two steps in this branch. The final steps
+	// both point to the same line. The difference between them is one
+	// step is before the execution of the digest and the other is
+	// after the execution of that digest.
+	returnpoint := &step{
+		in:     exitpoint,
+		next:   exitpoint,
+		out:    exitpoint,
+		parent: -1,
+	}
+
+	entrypoint = &step{
+		dgst:   dgst,
+		in:     returnpoint,
+		next:   returnpoint,
+		out:    exitpoint,
+		frame:  t.getStackFrame(dgst, nil),
+		parent: -1,
+	}
+
+	// Create a pseudo-frame and attach it to the return point.
+	// This is mostly used for getting the correct inputs utilized
+	// by this frame.
+	//
+	// We don't save this frame or assign it a unique ID as it should
+	// never be returned.
+	returnpoint.frame = &frame{
+		StackFrame: entrypoint.frame.StackFrame,
+		op: &pb.Op{
+			Inputs: []*pb.Input{
+				{Digest: string(dgst), Index: 0},
+			},
+		},
+	}
+
+	for {
+		// Construct the input step for this digest based on the inputs.
+		op := t.ops[entrypoint.dgst]
+		if len(op.Inputs) == 0 {
+			return entrypoint
 		}
 
-		op := t.ops[first.dgst]
-		if len(op.Inputs) > 0 {
-			parent := t.determineParent(op)
-			for i := len(op.Inputs) - 1; i >= 0; i-- {
-				if i == parent {
-					// Skip the direct parent.
-					continue
-				}
-				inp := op.Inputs[i]
-
-				// Create a pseudo-step that acts as an exit point for this
-				// branch. This step exists so this branch has a place to go
-				// after it has finished that will advance to the next
-				// instruction.
-				exit := &step{
-					in:    prev.in,
-					next:  prev.next,
-					out:   prev.out,
-					frame: prev.frame,
-				}
-
-				head := &step{
-					dgst:  digest.Digest(inp.Digest),
-					in:    exit,
-					next:  exit,
-					out:   exit,
-					frame: t.getStackFrame(digest.Digest(inp.Digest), nil),
-				}
-				prev.in = t.createBranch(head)
+		entrypoint.parent = t.determineParent(op)
+		for i := len(op.Inputs) - 1; i >= 0; i-- {
+			if i == entrypoint.parent {
+				// Skip the direct parent.
+				continue
 			}
 
-			// Set the digest of the parent input on the first step associated
-			// with this step if it exists.
-			if parent >= 0 {
-				prev.dgst = digest.Digest(op.Inputs[parent].Digest)
-			}
+			// When we find inputs that aren't the direct parent,
+			// we want to add them as a step before the current step.
+			// We have to do a few things when inserting this.
+			//
+			// 1. We move the digest from the old entrypoint to this node.
+			// 		This is so the breakpoint happens before these inputs
+			// 		are evaluated.
+			// 2. We keep the next/out pointers the same but redirect in
+			// 		to point to the new branch.
+			// 3. The direct parent is excluded from this logic. We handle
+			// 		that later.
+			inp := op.Inputs[i]
+
+			head := *entrypoint
+			entrypoint.dgst = ""
+
+			// Create the routine associated with this input.
+			// Associate it with the entrypoint in step.
+			head.in = t.createBranch(digest.Digest(inp.Digest), entrypoint)
+			entrypoint = &head
+		}
+
+		// If we have no direct parent, return the current entrypoint
+		// as the beginning.
+		if entrypoint.parent < 0 {
+			return entrypoint
 		}
 
-		// New first is the step we just created.
-		first = prev
+		// Create a new step that refers to the direct parent.
+		head := &step{
+			dgst:   digest.Digest(op.Inputs[entrypoint.parent].Digest),
+			in:     entrypoint,
+			next:   entrypoint,
+			out:    entrypoint.out,
+			parent: -1,
+		}
+		head.frame = t.getStackFrame(head.dgst, entrypoint)
+		entrypoint = head
 	}
-	return first
 }
 
 func (t *thread) getStackFrame(dgst digest.Digest, next *step) *frame {
@@ -274,18 +306,16 @@ func (t *thread) needsDebug(cur *step, step stepType, err error) (e dap.StoppedE
 	} else if cur != nil {
 		if step != stepContinue {
 			e.Reason = "step"
-		} else if next := cur.in; next != nil {
-			if id, ok := t.bps[next.dgst]; ok {
-				e.Reason = "breakpoint"
-				e.Description = "Paused on breakpoint"
-				e.HitBreakpointIds = []int{id}
-			}
+		} else if id, ok := t.bps[cur.dgst]; ok {
+			e.Reason = "breakpoint"
+			e.Description = "Paused on breakpoint"
+			e.HitBreakpointIds = []int{id}
 		}
 	}
 	return
 }
 
-func (t *thread) pause(c Context, ref gateway.Reference, err error, mounts map[string]gateway.Reference, pos *step, event dap.StoppedEventBody) <-chan stepType {
+func (t *thread) pause(c Context, k string, refs map[string]gateway.Reference, err error, pos *step, event dap.StoppedEventBody) <-chan stepType {
 	t.mu.Lock()
 	defer t.mu.Unlock()
 
@@ -295,10 +325,12 @@ func (t *thread) pause(c Context, ref gateway.Reference, err error, mounts map[s
 	t.paused = make(chan stepType, 1)
 
 	ctx, cancel := context.WithCancelCause(c)
-	t.collectStackTrace(ctx, pos, mounts)
+	t.collectStackTrace(ctx, pos, refs)
 	t.cancel = cancel
 
-	if ref != nil || err != nil {
+	// Used for exec. Only works if there was an error or if the step returns
+	// a root mount.
+	if ref, ok := refs[k]; ok || err != nil {
 		t.prepareResultHandle(c, ref, err)
 	}
 
@@ -412,7 +444,7 @@ func (t *thread) getLLBState(ctx Context) error {
 		dgst := digest.FromBytes(dt)
 
 		var op pb.Op
-		if err := op.UnmarshalVT(dt); err != nil {
+		if err := op.Unmarshal(dt); err != nil {
 			return err
 		}
 		t.ops[dgst] = &op
@@ -426,7 +458,7 @@ func (t *thread) setBreakpoints(ctx Context) {
 	t.bps = t.breakpointMap.Intersect(ctx, t.def.Source, t.sourcePath)
 }
 
-func (t *thread) seekNext(ctx Context, from *step, action stepType) (gateway.Reference, *step, map[string]gateway.Reference, error) {
+func (t *thread) seekNext(ctx Context, from *step, action stepType) (string, *step, map[string]gateway.Reference, error) {
 	// If we're at the end, return no digest to signal that
 	// we should conclude debugging.
 	var target *step
@@ -443,31 +475,26 @@ func (t *thread) seekNext(ctx Context, from *step, action stepType) (gateway.Ref
 	return t.seek(ctx, target)
 }
 
-func (t *thread) seek(ctx Context, target *step) (ref gateway.Reference, result *step, mounts map[string]gateway.Reference, err error) {
+func (t *thread) seek(ctx Context, target *step) (k string, result *step, mounts map[string]gateway.Reference, err error) {
+	k = "/"
+
+	var refs map[string]gateway.Reference
 	if target != nil {
-		if target.dgst != "" {
-			ref, err = t.solve(ctx, target.dgst, 0)
-			if err != nil {
-				return ref, nil, nil, err
-			}
+		k, refs, err = t.solveInputs(ctx, target)
+		if err != nil {
+			return "", nil, nil, err
 		}
-
 		result = target
 	} else {
-		ref = t.ref
+		refs = map[string]gateway.Reference{"/": t.ref}
 	}
 
-	if ref != nil {
-		if err = ref.Evaluate(ctx); err != nil {
-			result, mounts = t.rewind(ctx, err)
-			if result == nil {
-				return nil, nil, nil, err
-			}
-		} else {
-			mounts = map[string]gateway.Reference{"/": ref}
+	if len(refs) > 0 {
+		if err := t.evaluateRefs(ctx, refs); err != nil {
+			return t.rewind(ctx, err)
 		}
 	}
-	return ref, result, mounts, err
+	return k, result, refs, nil
 }
 
 func (t *thread) continueDigest(from *step) *step {
@@ -487,28 +514,74 @@ func (t *thread) continueDigest(from *step) *step {
 	next := func(s *step) *step {
 		cur := s.in
 		for cur != nil {
-			next := cur.in
-			if next != nil && isBreakpoint(next.dgst) {
+			if isBreakpoint(cur.dgst) {
 				return cur
 			}
-			cur = next
+			cur = cur.in
 		}
 		return nil
 	}
 	return next(from)
 }
 
-func (t *thread) solve(ctx context.Context, target digest.Digest, index int64) (gateway.Reference, error) {
-	if target == t.head {
+func (t *thread) solveInputs(ctx context.Context, target *step) (string, map[string]gateway.Reference, error) {
+	if target == nil || target.frame.op == nil {
+		return "", nil, nil
+	}
+	op := target.frame.op
+
+	var root string
+	refs := make(map[string]gateway.Reference)
+	for i, input := range op.Inputs {
+		k := t.determineInputName(op, input)
+		if _, ok := refs[k]; ok || k == "" {
+			continue
+		}
+
+		if i == target.parent {
+			root = k
+		}
+
+		ref, err := t.solve(ctx, input)
+		if err != nil {
+			return "", nil, err
+		}
+		refs[k] = ref
+	}
+	return root, refs, nil
+}
+
+func (t *thread) determineInputName(op *pb.Op, input *pb.Input) string {
+	switch op := op.Op.(type) {
+	case *pb.Op_Exec:
+		for _, m := range op.Exec.Mounts {
+			if m.Input >= 0 && m.Input == input.Index {
+				return m.Dest
+			}
+		}
+	}
+	return input.Digest
+}
+
+func (t *thread) evaluateRefs(ctx context.Context, refs map[string]gateway.Reference) error {
+	eg, _ := errgroup.WithContext(ctx)
+	for _, ref := range refs {
+		eg.Go(func() error {
+			return ref.Evaluate(ctx)
+		})
+	}
+	return eg.Wait()
+}
+
+func (t *thread) solve(ctx context.Context, input *pb.Input) (gateway.Reference, error) {
+	if input.Digest == string(t.head) {
 		return t.ref, nil
 	}
 
 	head := &pb.Op{
-		Inputs: []*pb.Input{
-			{Digest: string(target), Index: index},
-		},
+		Inputs: []*pb.Input{input},
 	}
-	dt, err := head.MarshalVT()
+	dt, err := head.Marshal()
 	if err != nil {
 		return nil, err
 	}
@@ -565,47 +638,37 @@ func (t *thread) hasFrame(id int) bool {
 	return ok
 }
 
-func (t *thread) rewind(ctx context.Context, err error) (result *step, mounts map[string]gateway.Reference) {
+func (t *thread) rewind(ctx Context, inErr error) (k string, result *step, mounts map[string]gateway.Reference, retErr error) {
 	var solveErr *errdefs.SolveError
-	if !errors.As(err, &solveErr) {
+	if !errors.As(inErr, &solveErr) {
 		// If this is not a solve error, do not return the
 		// reference and target step.
-		return nil, nil
+		return "", nil, nil, inErr
 	}
 
-	dt, err := solveErr.Op.MarshalVT()
+	dt, err := solveErr.Op.Marshal()
 	if err != nil {
-		return nil, nil
+		return "", nil, nil, err
 	}
 
 	// Find the error digest.
 	errDgst := digest.FromBytes(dt)
 
-	// Iterate from the first step to find the one
-	// we failed on.
+	// Iterate from the first step to find the one we failed on.
 	result = t.entrypoint
-	for result != nil {
-		next := result.in
-		if next != nil && next.dgst == errDgst {
-			break
-		}
-		result = next
+	for result != nil && result.dgst != errDgst {
+		result = result.in
 	}
 
-	if exec, ok := solveErr.Op.GetOp().(*pb.Op_Exec); ok {
-		mounts = make(map[string]gateway.Reference, len(exec.Exec.Mounts))
-		for _, m := range exec.Exec.Mounts {
-			if m.Input < 0 {
-				continue
-			}
+	if result == nil {
+		return "", nil, nil, inErr
+	}
 
-			// TODO: Use the correct input index for this.
-			input := solveErr.Op.Inputs[m.Input]
-			target, index := digest.Digest(input.Digest), input.Index
-			if ref, err := t.solve(ctx, target, index); err == nil {
-				mounts[m.Dest] = ref
-			}
-		}
+	// Seek to this step. This should succeed because otherwise
+	// we wouldn't have been able to even fail on it to begin with.
+	k, result, mounts, err = t.seek(ctx, result)
+	if err != nil {
+		return k, result, mounts, err
 	}
-	return result, mounts
+	return k, result, mounts, inErr
 }
diff --git a/dap/variables.go b/dap/variables.go
index 69959a30..8c19857a 100644
--- a/dap/variables.go
+++ b/dap/variables.go
@@ -4,8 +4,11 @@ import (
 	"context"
 	"fmt"
 	"io/fs"
+	"maps"
 	"path"
 	"path/filepath"
+	"slices"
+	"sort"
 	"strconv"
 	"strings"
 	"sync"
@@ -191,18 +194,29 @@ func (f *frame) fillVarsFromResult(ctx context.Context, mounts map[string]gatewa
 		Name:             "File Explorer",
 		PresentationHint: "locals",
 		VariablesReference: refs.New(func() []dap.Variable {
-			return fsVars(ctx, mounts, "/", refs)
+			return fileExplorer(ctx, mounts, refs)
 		}),
 		Expensive: true,
 	})
 }
 
-func fsVars(ctx context.Context, mounts map[string]gateway.Reference, path string, vars *variableReferences) []dap.Variable {
-	path, ref := lookupPath(path, mounts)
-	if ref == nil {
-		return nil
+func fileExplorer(ctx context.Context, mounts map[string]gateway.Reference, vars *variableReferences) []dap.Variable {
+	keys := slices.Collect(maps.Keys(mounts))
+	sort.Strings(keys)
+
+	outVars := make([]dap.Variable, 0, len(keys))
+	for _, k := range keys {
+		outVars = append(outVars, dap.Variable{
+			Name: k,
+			VariablesReference: vars.New(func() []dap.Variable {
+				return fsVars(ctx, mounts[k], "/", vars)
+			}),
+		})
 	}
+	return outVars
+}
 
+func fsVars(ctx context.Context, ref gateway.Reference, path string, vars *variableReferences) []dap.Variable {
 	files, err := ref.ReadDir(ctx, gateway.ReadDirRequest{
 		Path: path,
 	})
@@ -233,7 +247,7 @@ func fsVars(ctx context.Context, mounts map[string]gateway.Reference, path strin
 						return statVars(file)
 					}),
 				}
-				return append([]dap.Variable{dvar}, fsVars(ctx, mounts, fullpath, vars)...)
+				return append([]dap.Variable{dvar}, fsVars(ctx, ref, fullpath, vars)...)
 			})
 			fv.Value = ""
 		} else {
@@ -441,15 +455,3 @@ func betterLocation(r *pb.Range, f *frame, next *step) bool {
 	// Doesn't seem to be a better location.
 	return false
 }
-
-func lookupPath(path string, mounts map[string]gateway.Reference) (remainder string, ref gateway.Reference) {
-	var prefix string
-	for p, r := range mounts {
-		if len(p) > len(prefix) && strings.HasPrefix(path, p) {
-			prefix = p
-			remainder, _ = filepath.Rel(prefix, path)
-			ref = r
-		}
-	}
-	return "/" + remainder, ref
-}
diff --git a/driver/docker-container/driver.go b/driver/docker-container/driver.go
index ce37fcea..4f26f17c 100644
--- a/driver/docker-container/driver.go
+++ b/driver/docker-container/driver.go
@@ -16,6 +16,7 @@ import (
 	"github.com/docker/buildx/driver"
 	"github.com/docker/buildx/driver/bkimage"
 	"github.com/docker/buildx/util/confutil"
+	"github.com/docker/buildx/util/ghutil"
 	"github.com/docker/buildx/util/imagetools"
 	"github.com/docker/buildx/util/progress"
 	"github.com/docker/cli/cli/context/docker"
@@ -43,20 +44,21 @@ type Driver struct {
 
 	// if you add fields, remember to update docs:
 	// https://github.com/docker/docs/blob/main/content/build/drivers/docker-container.md
-	netMode       string
-	image         string
-	memory        opts.MemBytes
-	memorySwap    opts.MemSwapBytes
-	cpuQuota      int64
-	cpuPeriod     int64
-	cpuShares     int64
-	cpusetCpus    string
-	cpusetMems    string
-	cgroupParent  string
-	restartPolicy container.RestartPolicy
-	env           []string
-	defaultLoad   bool
-	gpus          []container.DeviceRequest
+	netMode            string
+	image              string
+	memory             opts.MemBytes
+	memorySwap         opts.MemSwapBytes
+	cpuQuota           int64
+	cpuPeriod          int64
+	cpuShares          int64
+	cpusetCpus         string
+	cpusetMems         string
+	cgroupParent       string
+	restartPolicy      container.RestartPolicy
+	env                []string
+	defaultLoad        bool
+	gpus               []container.DeviceRequest
+	writeProvenanceGHA bool
 }
 
 func (d *Driver) IsMobyDriver() bool {
@@ -137,6 +139,17 @@ func (d *Driver) create(ctx context.Context, l progress.SubLogger) error {
 			},
 		}
 
+		if d.writeProvenanceGHA {
+			if ghactx, err := ghutil.GithubActionsContext(); err != nil {
+				return err
+			} else if ghactx != nil {
+				if d.Files == nil {
+					d.Files = make(map[string][]byte)
+				}
+				d.Files["provenance.d/github_actions_context.json"] = ghactx
+			}
+		}
+
 		// Mount WSL libaries if running in WSL environment and Docker context
 		// is a local socket as requesting GPU on container builder creation
 		// is not enough when generating the CDI specification for GPU devices.
diff --git a/driver/docker-container/factory.go b/driver/docker-container/factory.go
index dba791b6..06e9c503 100644
--- a/driver/docker-container/factory.go
+++ b/driver/docker-container/factory.go
@@ -47,9 +47,10 @@ func (f *factory) New(ctx context.Context, cfg driver.InitConfig) (driver.Driver
 		return nil, err
 	}
 	d := &Driver{
-		factory:       f,
-		InitConfig:    cfg,
-		restartPolicy: rp,
+		factory:            f,
+		InitConfig:         cfg,
+		restartPolicy:      rp,
+		writeProvenanceGHA: true,
 	}
 	var gpus dockeropts.GpuOpts
 	if err := gpus.Set("all"); err == nil {
@@ -111,6 +112,11 @@ func (f *factory) New(ctx context.Context, cfg driver.InitConfig) (driver.Driver
 				return nil, errors.Errorf("invalid env option %q, expecting env.FOO=bar", k)
 			}
 			d.env = append(d.env, fmt.Sprintf("%s=%s", envName, v))
+		case k == "provenance-add-gha":
+			d.writeProvenanceGHA, err = strconv.ParseBool(v)
+			if err != nil {
+				return nil, err
+			}
 		default:
 			return nil, errors.Errorf("invalid driver option %s for docker-container driver", k)
 		}
diff --git a/util/ghutil/fixtures/ghactx_pr.env b/util/ghutil/fixtures/ghactx_pr.env
new file mode 100644
index 00000000..0795bb72
--- /dev/null
+++ b/util/ghutil/fixtures/ghactx_pr.env
@@ -0,0 +1,106 @@
+ACCEPT_EULA=Y
+ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE=/opt/actionarchivecache
+AGENT_TOOLSDIRECTORY=/opt/hostedtoolcache
+ANDROID_HOME=/usr/local/lib/android/sdk
+ANDROID_NDK=/usr/local/lib/android/sdk/ndk/27.3.13750724
+ANDROID_NDK_HOME=/usr/local/lib/android/sdk/ndk/27.3.13750724
+ANDROID_NDK_LATEST_HOME=/usr/local/lib/android/sdk/ndk/28.2.13676358
+ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/27.3.13750724
+ANDROID_SDK_ROOT=/usr/local/lib/android/sdk
+ANT_HOME=/usr/share/ant
+AZURE_EXTENSION_DIR=/opt/az/azcliextensions
+BOOTSTRAP_HASKELL_NONINTERACTIVE=1
+CHROMEWEBDRIVER=/usr/local/share/chromedriver-linux64
+CHROME_BIN=/usr/bin/google-chrome
+CI=true
+CONDA=/usr/share/miniconda
+DEBIAN_FRONTEND=noninteractive
+DOTNET_MULTILEVEL_LOOKUP=0
+DOTNET_NOLOGO=1
+DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
+EDGEWEBDRIVER=/usr/local/share/edge_driver
+ENABLE_RUNNER_TRACING=true
+GECKOWEBDRIVER=/usr/local/share/gecko_driver
+GHCUP_INSTALL_BASE_PREFIX=/usr/local
+GITHUB_ACTION=__run
+GITHUB_ACTIONS=true
+GITHUB_ACTION_REF=
+GITHUB_ACTION_REPOSITORY=
+GITHUB_ACTOR=crazy-max
+GITHUB_ACTOR_ID=1951866
+GITHUB_API_URL=https://api.github.com
+GITHUB_BASE_REF=master
+GITHUB_ENV=/home/runner/work/_temp/_runner_file_commands/set_env_33a5ccf0-43cf-4794-91bc-98abed829158
+GITHUB_EVENT_NAME=pull_request
+GITHUB_EVENT_PATH=./fixtures/ghactx_pr_event.json
+GITHUB_GRAPHQL_URL=https://api.github.com/graphql
+GITHUB_HEAD_REF=ghutil
+GITHUB_JOB=ghutil
+GITHUB_OUTPUT=/home/runner/work/_temp/_runner_file_commands/set_output_33a5ccf0-43cf-4794-91bc-98abed829158
+GITHUB_PATH=/home/runner/work/_temp/_runner_file_commands/add_path_33a5ccf0-43cf-4794-91bc-98abed829158
+GITHUB_REF=refs/pull/55/merge
+GITHUB_REF_NAME=55/merge
+GITHUB_REF_PROTECTED=false
+GITHUB_REF_TYPE=branch
+GITHUB_REPOSITORY=docker/test-docker-action
+GITHUB_REPOSITORY_ID=285789493
+GITHUB_REPOSITORY_OWNER=docker
+GITHUB_REPOSITORY_OWNER_ID=5429470
+GITHUB_RETENTION_DAYS=90
+GITHUB_RUN_ATTEMPT=1
+GITHUB_RUN_ID=18774179609
+GITHUB_RUN_NUMBER=2
+GITHUB_SERVER_URL=https://github.com
+GITHUB_SHA=198c644a687204e604c8476ad23d7d5bbb0dc221
+GITHUB_STATE=/home/runner/work/_temp/_runner_file_commands/save_state_33a5ccf0-43cf-4794-91bc-98abed829158
+GITHUB_STEP_SUMMARY=/home/runner/work/_temp/_runner_file_commands/step_summary_33a5ccf0-43cf-4794-91bc-98abed829158
+GITHUB_TRIGGERING_ACTOR=crazy-max
+GITHUB_WORKFLOW=ghutil
+GITHUB_WORKFLOW_REF=docker/test-docker-action/.github/workflows/ghutil.yml@refs/pull/55/merge
+GITHUB_WORKFLOW_SHA=198c644a687204e604c8476ad23d7d5bbb0dc221
+GITHUB_WORKSPACE=/home/runner/work/test-docker-action/test-docker-action
+GOROOT_1_22_X64=/opt/hostedtoolcache/go/1.22.12/x64
+GOROOT_1_23_X64=/opt/hostedtoolcache/go/1.23.12/x64
+GOROOT_1_24_X64=/opt/hostedtoolcache/go/1.24.7/x64
+GRADLE_HOME=/usr/share/gradle-9.1.0
+HOME=/home/runner
+HOMEBREW_CLEANUP_PERIODIC_FULL_DAYS=3650
+HOMEBREW_NO_AUTO_UPDATE=1
+INVOCATION_ID=d3fd0f4a6372458dbf53aa6d6abd62b0
+ImageOS=ubuntu24
+ImageVersion=20250929.60.1
+JAVA_HOME=/usr/lib/jvm/temurin-17-jdk-amd64
+JAVA_HOME_11_X64=/usr/lib/jvm/temurin-11-jdk-amd64
+JAVA_HOME_17_X64=/usr/lib/jvm/temurin-17-jdk-amd64
+JAVA_HOME_21_X64=/usr/lib/jvm/temurin-21-jdk-amd64
+JAVA_HOME_25_X64=/usr/lib/jvm/temurin-25-jdk-amd64
+JAVA_HOME_8_X64=/usr/lib/jvm/temurin-8-jdk-amd64
+JOURNAL_STREAM=9:12643
+LANG=C.UTF-8
+LOGNAME=runner
+MEMORY_PRESSURE_WATCH=/sys/fs/cgroup/system.slice/hosted-compute-agent.service/memory.pressure
+MEMORY_PRESSURE_WRITE=c29tZSAyMDAwMDAgMjAwMDAwMAA=
+NVM_DIR=/home/runner/.nvm
+PATH=/snap/bin:/home/runner/.local/bin:/opt/pipx_bin:/home/runner/.cargo/bin:/home/runner/.config/composer/vendor/bin:/usr/local/.ghcup/bin:/home/runner/.dotnet/tools:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
+PIPX_BIN_DIR=/opt/pipx_bin
+PIPX_HOME=/opt/pipx
+POWERSHELL_DISTRIBUTION_CHANNEL=GitHub-Actions-ubuntu24
+RUNNER_ARCH=X64
+RUNNER_ENVIRONMENT=github-hosted
+RUNNER_NAME=GitHub Actions 1002237010
+RUNNER_OS=Linux
+RUNNER_TEMP=/home/runner/work/_temp
+RUNNER_TOOL_CACHE=/opt/hostedtoolcache
+RUNNER_TRACKING_ID=github_aef5ac41-9cf7-4195-9da9-851d8b29e68e
+RUNNER_WORKSPACE=/home/runner/work/test-docker-action
+SELENIUM_JAR_PATH=/usr/share/java/selenium-server.jar
+SGX_AESM_ADDR=1
+SHELL=/bin/bash
+SHLVL=1
+SWIFT_PATH=/usr/share/swift/usr/bin
+SYSTEMD_EXEC_PID=1762
+USER=runner
+VCPKG_INSTALLATION_ROOT=/usr/local/share/vcpkg
+XDG_CONFIG_HOME=/home/runner/.config
+XDG_RUNTIME_DIR=/run/user/1001
+_=/usr/bin/env
\ No newline at end of file
diff --git a/util/ghutil/fixtures/ghactx_pr_event.json b/util/ghutil/fixtures/ghactx_pr_event.json
new file mode 100644
index 00000000..42bd2190
--- /dev/null
+++ b/util/ghutil/fixtures/ghactx_pr_event.json
@@ -0,0 +1,542 @@
+{
+  "action": "synchronize",
+  "after": "3be79a9f0a68f4a7b93c001d377d9397663f6a5f",
+  "before": "c89d99c85a2603825bf11a688a446cfb50488deb",
+  "enterprise": {
+    "avatar_url": "https://avatars.githubusercontent.com/b/19176?v=4",
+    "created_at": "2022-12-30T23:53:17Z",
+    "description": null,
+    "html_url": "https://github.com/enterprises/docker",
+    "id": 19176,
+    "name": "Docker",
+    "node_id": "E_kgDNSug",
+    "slug": "docker",
+    "updated_at": "2025-10-20T20:39:05Z",
+    "website_url": null
+  },
+  "number": 55,
+  "organization": {
+    "avatar_url": "https://avatars.githubusercontent.com/u/5429470?v=4",
+    "description": "Docker helps developers bring their ideas to life by conquering the complexity of app development.",
+    "events_url": "https://api.github.com/orgs/docker/events",
+    "hooks_url": "https://api.github.com/orgs/docker/hooks",
+    "id": 5429470,
+    "issues_url": "https://api.github.com/orgs/docker/issues",
+    "login": "docker",
+    "members_url": "https://api.github.com/orgs/docker/members{/member}",
+    "node_id": "MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+    "public_members_url": "https://api.github.com/orgs/docker/public_members{/member}",
+    "repos_url": "https://api.github.com/orgs/docker/repos",
+    "url": "https://api.github.com/orgs/docker"
+  },
+  "pull_request": {
+    "_links": {
+      "comments": {
+        "href": "https://api.github.com/repos/docker/test-docker-action/issues/55/comments"
+      },
+      "commits": {
+        "href": "https://api.github.com/repos/docker/test-docker-action/pulls/55/commits"
+      },
+      "html": {
+        "href": "https://github.com/docker/test-docker-action/pull/55"
+      },
+      "issue": {
+        "href": "https://api.github.com/repos/docker/test-docker-action/issues/55"
+      },
+      "review_comment": {
+        "href": "https://api.github.com/repos/docker/test-docker-action/pulls/comments{/number}"
+      },
+      "review_comments": {
+        "href": "https://api.github.com/repos/docker/test-docker-action/pulls/55/comments"
+      },
+      "self": {
+        "href": "https://api.github.com/repos/docker/test-docker-action/pulls/55"
+      },
+      "statuses": {
+        "href": "https://api.github.com/repos/docker/test-docker-action/statuses/3be79a9f0a68f4a7b93c001d377d9397663f6a5f"
+      }
+    },
+    "active_lock_reason": null,
+    "additions": 25,
+    "assignee": null,
+    "assignees": [],
+    "author_association": "MEMBER",
+    "auto_merge": null,
+    "base": {
+      "label": "docker:master",
+      "ref": "master",
+      "repo": {
+        "allow_auto_merge": false,
+        "allow_forking": true,
+        "allow_merge_commit": true,
+        "allow_rebase_merge": true,
+        "allow_squash_merge": true,
+        "allow_update_branch": false,
+        "archive_url": "https://api.github.com/repos/docker/test-docker-action/{archive_format}{/ref}",
+        "archived": false,
+        "assignees_url": "https://api.github.com/repos/docker/test-docker-action/assignees{/user}",
+        "blobs_url": "https://api.github.com/repos/docker/test-docker-action/git/blobs{/sha}",
+        "branches_url": "https://api.github.com/repos/docker/test-docker-action/branches{/branch}",
+        "clone_url": "https://github.com/docker/test-docker-action.git",
+        "collaborators_url": "https://api.github.com/repos/docker/test-docker-action/collaborators{/collaborator}",
+        "comments_url": "https://api.github.com/repos/docker/test-docker-action/comments{/number}",
+        "commits_url": "https://api.github.com/repos/docker/test-docker-action/commits{/sha}",
+        "compare_url": "https://api.github.com/repos/docker/test-docker-action/compare/{base}...{head}",
+        "contents_url": "https://api.github.com/repos/docker/test-docker-action/contents/{+path}",
+        "contributors_url": "https://api.github.com/repos/docker/test-docker-action/contributors",
+        "created_at": "2020-08-07T09:23:00Z",
+        "default_branch": "master",
+        "delete_branch_on_merge": false,
+        "deployments_url": "https://api.github.com/repos/docker/test-docker-action/deployments",
+        "description": "Test \"Docker\" Actions",
+        "disabled": false,
+        "downloads_url": "https://api.github.com/repos/docker/test-docker-action/downloads",
+        "events_url": "https://api.github.com/repos/docker/test-docker-action/events",
+        "fork": false,
+        "forks": 1,
+        "forks_count": 1,
+        "forks_url": "https://api.github.com/repos/docker/test-docker-action/forks",
+        "full_name": "docker/test-docker-action",
+        "git_commits_url": "https://api.github.com/repos/docker/test-docker-action/git/commits{/sha}",
+        "git_refs_url": "https://api.github.com/repos/docker/test-docker-action/git/refs{/sha}",
+        "git_tags_url": "https://api.github.com/repos/docker/test-docker-action/git/tags{/sha}",
+        "git_url": "git://github.com/docker/test-docker-action.git",
+        "has_discussions": false,
+        "has_downloads": true,
+        "has_issues": true,
+        "has_pages": false,
+        "has_projects": false,
+        "has_wiki": false,
+        "homepage": "",
+        "hooks_url": "https://api.github.com/repos/docker/test-docker-action/hooks",
+        "html_url": "https://github.com/docker/test-docker-action",
+        "id": 285789493,
+        "is_template": false,
+        "issue_comment_url": "https://api.github.com/repos/docker/test-docker-action/issues/comments{/number}",
+        "issue_events_url": "https://api.github.com/repos/docker/test-docker-action/issues/events{/number}",
+        "issues_url": "https://api.github.com/repos/docker/test-docker-action/issues{/number}",
+        "keys_url": "https://api.github.com/repos/docker/test-docker-action/keys{/key_id}",
+        "labels_url": "https://api.github.com/repos/docker/test-docker-action/labels{/name}",
+        "language": "JavaScript",
+        "languages_url": "https://api.github.com/repos/docker/test-docker-action/languages",
+        "license": {
+          "key": "mit",
+          "name": "MIT License",
+          "node_id": "MDc6TGljZW5zZTEz",
+          "spdx_id": "MIT",
+          "url": "https://api.github.com/licenses/mit"
+        },
+        "merge_commit_message": "PR_TITLE",
+        "merge_commit_title": "MERGE_MESSAGE",
+        "merges_url": "https://api.github.com/repos/docker/test-docker-action/merges",
+        "milestones_url": "https://api.github.com/repos/docker/test-docker-action/milestones{/number}",
+        "mirror_url": null,
+        "name": "test-docker-action",
+        "node_id": "MDEwOlJlcG9zaXRvcnkyODU3ODk0OTM=",
+        "notifications_url": "https://api.github.com/repos/docker/test-docker-action/notifications{?since,all,participating}",
+        "open_issues": 16,
+        "open_issues_count": 16,
+        "owner": {
+          "avatar_url": "https://avatars.githubusercontent.com/u/5429470?v=4",
+          "events_url": "https://api.github.com/users/docker/events{/privacy}",
+          "followers_url": "https://api.github.com/users/docker/followers",
+          "following_url": "https://api.github.com/users/docker/following{/other_user}",
+          "gists_url": "https://api.github.com/users/docker/gists{/gist_id}",
+          "gravatar_id": "",
+          "html_url": "https://github.com/docker",
+          "id": 5429470,
+          "login": "docker",
+          "node_id": "MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+          "organizations_url": "https://api.github.com/users/docker/orgs",
+          "received_events_url": "https://api.github.com/users/docker/received_events",
+          "repos_url": "https://api.github.com/users/docker/repos",
+          "site_admin": false,
+          "starred_url": "https://api.github.com/users/docker/starred{/owner}{/repo}",
+          "subscriptions_url": "https://api.github.com/users/docker/subscriptions",
+          "type": "Organization",
+          "url": "https://api.github.com/users/docker",
+          "user_view_type": "public"
+        },
+        "private": true,
+        "pulls_url": "https://api.github.com/repos/docker/test-docker-action/pulls{/number}",
+        "pushed_at": "2025-09-29T14:04:23Z",
+        "releases_url": "https://api.github.com/repos/docker/test-docker-action/releases{/id}",
+        "size": 1374,
+        "squash_merge_commit_message": "COMMIT_MESSAGES",
+        "squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
+        "ssh_url": "git@github.com:docker/test-docker-action.git",
+        "stargazers_count": 1,
+        "stargazers_url": "https://api.github.com/repos/docker/test-docker-action/stargazers",
+        "statuses_url": "https://api.github.com/repos/docker/test-docker-action/statuses/{sha}",
+        "subscribers_url": "https://api.github.com/repos/docker/test-docker-action/subscribers",
+        "subscription_url": "https://api.github.com/repos/docker/test-docker-action/subscription",
+        "svn_url": "https://github.com/docker/test-docker-action",
+        "tags_url": "https://api.github.com/repos/docker/test-docker-action/tags",
+        "teams_url": "https://api.github.com/repos/docker/test-docker-action/teams",
+        "topics": [],
+        "trees_url": "https://api.github.com/repos/docker/test-docker-action/git/trees{/sha}",
+        "updated_at": "2025-09-29T14:04:27Z",
+        "url": "https://api.github.com/repos/docker/test-docker-action",
+        "use_squash_pr_title_as_default": false,
+        "visibility": "internal",
+        "watchers": 1,
+        "watchers_count": 1,
+        "web_commit_signoff_required": false
+      },
+      "sha": "231cbfa88dca156930d8e7991b6cf57bc09fd090",
+      "user": {
+        "avatar_url": "https://avatars.githubusercontent.com/u/5429470?v=4",
+        "events_url": "https://api.github.com/users/docker/events{/privacy}",
+        "followers_url": "https://api.github.com/users/docker/followers",
+        "following_url": "https://api.github.com/users/docker/following{/other_user}",
+        "gists_url": "https://api.github.com/users/docker/gists{/gist_id}",
+        "gravatar_id": "",
+        "html_url": "https://github.com/docker",
+        "id": 5429470,
+        "login": "docker",
+        "node_id": "MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+        "organizations_url": "https://api.github.com/users/docker/orgs",
+        "received_events_url": "https://api.github.com/users/docker/received_events",
+        "repos_url": "https://api.github.com/users/docker/repos",
+        "site_admin": false,
+        "starred_url": "https://api.github.com/users/docker/starred{/owner}{/repo}",
+        "subscriptions_url": "https://api.github.com/users/docker/subscriptions",
+        "type": "Organization",
+        "url": "https://api.github.com/users/docker",
+        "user_view_type": "public"
+      }
+    },
+    "body": null,
+    "changed_files": 1,
+    "closed_at": null,
+    "comments": 0,
+    "comments_url": "https://api.github.com/repos/docker/test-docker-action/issues/55/comments",
+    "commits": 1,
+    "commits_url": "https://api.github.com/repos/docker/test-docker-action/pulls/55/commits",
+    "created_at": "2025-10-24T08:25:26Z",
+    "deletions": 0,
+    "diff_url": "https://github.com/docker/test-docker-action/pull/55.diff",
+    "draft": false,
+    "head": {
+      "label": "crazy-max:ghutil",
+      "ref": "ghutil",
+      "repo": {
+        "allow_auto_merge": false,
+        "allow_forking": true,
+        "allow_merge_commit": true,
+        "allow_rebase_merge": true,
+        "allow_squash_merge": true,
+        "allow_update_branch": false,
+        "archive_url": "https://api.github.com/repos/crazy-max/test-docker-action/{archive_format}{/ref}",
+        "archived": false,
+        "assignees_url": "https://api.github.com/repos/crazy-max/test-docker-action/assignees{/user}",
+        "blobs_url": "https://api.github.com/repos/crazy-max/test-docker-action/git/blobs{/sha}",
+        "branches_url": "https://api.github.com/repos/crazy-max/test-docker-action/branches{/branch}",
+        "clone_url": "https://github.com/crazy-max/test-docker-action.git",
+        "collaborators_url": "https://api.github.com/repos/crazy-max/test-docker-action/collaborators{/collaborator}",
+        "comments_url": "https://api.github.com/repos/crazy-max/test-docker-action/comments{/number}",
+        "commits_url": "https://api.github.com/repos/crazy-max/test-docker-action/commits{/sha}",
+        "compare_url": "https://api.github.com/repos/crazy-max/test-docker-action/compare/{base}...{head}",
+        "contents_url": "https://api.github.com/repos/crazy-max/test-docker-action/contents/{+path}",
+        "contributors_url": "https://api.github.com/repos/crazy-max/test-docker-action/contributors",
+        "created_at": "2021-07-11T17:34:22Z",
+        "default_branch": "master",
+        "delete_branch_on_merge": false,
+        "deployments_url": "https://api.github.com/repos/crazy-max/test-docker-action/deployments",
+        "description": "Test \"Docker\" Actions",
+        "disabled": false,
+        "downloads_url": "https://api.github.com/repos/crazy-max/test-docker-action/downloads",
+        "events_url": "https://api.github.com/repos/crazy-max/test-docker-action/events",
+        "fork": true,
+        "forks": 0,
+        "forks_count": 0,
+        "forks_url": "https://api.github.com/repos/crazy-max/test-docker-action/forks",
+        "full_name": "crazy-max/test-docker-action",
+        "git_commits_url": "https://api.github.com/repos/crazy-max/test-docker-action/git/commits{/sha}",
+        "git_refs_url": "https://api.github.com/repos/crazy-max/test-docker-action/git/refs{/sha}",
+        "git_tags_url": "https://api.github.com/repos/crazy-max/test-docker-action/git/tags{/sha}",
+        "git_url": "git://github.com/crazy-max/test-docker-action.git",
+        "has_discussions": false,
+        "has_downloads": true,
+        "has_issues": false,
+        "has_pages": false,
+        "has_projects": true,
+        "has_wiki": false,
+        "homepage": "",
+        "hooks_url": "https://api.github.com/repos/crazy-max/test-docker-action/hooks",
+        "html_url": "https://github.com/crazy-max/test-docker-action",
+        "id": 385013169,
+        "is_template": false,
+        "issue_comment_url": "https://api.github.com/repos/crazy-max/test-docker-action/issues/comments{/number}",
+        "issue_events_url": "https://api.github.com/repos/crazy-max/test-docker-action/issues/events{/number}",
+        "issues_url": "https://api.github.com/repos/crazy-max/test-docker-action/issues{/number}",
+        "keys_url": "https://api.github.com/repos/crazy-max/test-docker-action/keys{/key_id}",
+        "labels_url": "https://api.github.com/repos/crazy-max/test-docker-action/labels{/name}",
+        "language": "JavaScript",
+        "languages_url": "https://api.github.com/repos/crazy-max/test-docker-action/languages",
+        "license": {
+          "key": "mit",
+          "name": "MIT License",
+          "node_id": "MDc6TGljZW5zZTEz",
+          "spdx_id": "MIT",
+          "url": "https://api.github.com/licenses/mit"
+        },
+        "merge_commit_message": "PR_TITLE",
+        "merge_commit_title": "MERGE_MESSAGE",
+        "merges_url": "https://api.github.com/repos/crazy-max/test-docker-action/merges",
+        "milestones_url": "https://api.github.com/repos/crazy-max/test-docker-action/milestones{/number}",
+        "mirror_url": null,
+        "name": "test-docker-action",
+        "node_id": "MDEwOlJlcG9zaXRvcnkzODUwMTMxNjk=",
+        "notifications_url": "https://api.github.com/repos/crazy-max/test-docker-action/notifications{?since,all,participating}",
+        "open_issues": 0,
+        "open_issues_count": 0,
+        "owner": {
+          "avatar_url": "https://avatars.githubusercontent.com/u/1951866?v=4",
+          "events_url": "https://api.github.com/users/crazy-max/events{/privacy}",
+          "followers_url": "https://api.github.com/users/crazy-max/followers",
+          "following_url": "https://api.github.com/users/crazy-max/following{/other_user}",
+          "gists_url": "https://api.github.com/users/crazy-max/gists{/gist_id}",
+          "gravatar_id": "",
+          "html_url": "https://github.com/crazy-max",
+          "id": 1951866,
+          "login": "crazy-max",
+          "node_id": "MDQ6VXNlcjE5NTE4NjY=",
+          "organizations_url": "https://api.github.com/users/crazy-max/orgs",
+          "received_events_url": "https://api.github.com/users/crazy-max/received_events",
+          "repos_url": "https://api.github.com/users/crazy-max/repos",
+          "site_admin": false,
+          "starred_url": "https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
+          "subscriptions_url": "https://api.github.com/users/crazy-max/subscriptions",
+          "type": "User",
+          "url": "https://api.github.com/users/crazy-max",
+          "user_view_type": "public"
+        },
+        "private": true,
+        "pulls_url": "https://api.github.com/repos/crazy-max/test-docker-action/pulls{/number}",
+        "pushed_at": "2025-10-24T08:27:13Z",
+        "releases_url": "https://api.github.com/repos/crazy-max/test-docker-action/releases{/id}",
+        "size": 1078,
+        "squash_merge_commit_message": "COMMIT_MESSAGES",
+        "squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
+        "ssh_url": "git@github.com:crazy-max/test-docker-action.git",
+        "stargazers_count": 0,
+        "stargazers_url": "https://api.github.com/repos/crazy-max/test-docker-action/stargazers",
+        "statuses_url": "https://api.github.com/repos/crazy-max/test-docker-action/statuses/{sha}",
+        "subscribers_url": "https://api.github.com/repos/crazy-max/test-docker-action/subscribers",
+        "subscription_url": "https://api.github.com/repos/crazy-max/test-docker-action/subscription",
+        "svn_url": "https://github.com/crazy-max/test-docker-action",
+        "tags_url": "https://api.github.com/repos/crazy-max/test-docker-action/tags",
+        "teams_url": "https://api.github.com/repos/crazy-max/test-docker-action/teams",
+        "topics": [],
+        "trees_url": "https://api.github.com/repos/crazy-max/test-docker-action/git/trees{/sha}",
+        "updated_at": "2025-08-12T06:56:46Z",
+        "url": "https://api.github.com/repos/crazy-max/test-docker-action",
+        "use_squash_pr_title_as_default": false,
+        "visibility": "private",
+        "watchers": 0,
+        "watchers_count": 0,
+        "web_commit_signoff_required": false
+      },
+      "sha": "3be79a9f0a68f4a7b93c001d377d9397663f6a5f",
+      "user": {
+        "avatar_url": "https://avatars.githubusercontent.com/u/1951866?v=4",
+        "events_url": "https://api.github.com/users/crazy-max/events{/privacy}",
+        "followers_url": "https://api.github.com/users/crazy-max/followers",
+        "following_url": "https://api.github.com/users/crazy-max/following{/other_user}",
+        "gists_url": "https://api.github.com/users/crazy-max/gists{/gist_id}",
+        "gravatar_id": "",
+        "html_url": "https://github.com/crazy-max",
+        "id": 1951866,
+        "login": "crazy-max",
+        "node_id": "MDQ6VXNlcjE5NTE4NjY=",
+        "organizations_url": "https://api.github.com/users/crazy-max/orgs",
+        "received_events_url": "https://api.github.com/users/crazy-max/received_events",
+        "repos_url": "https://api.github.com/users/crazy-max/repos",
+        "site_admin": false,
+        "starred_url": "https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
+        "subscriptions_url": "https://api.github.com/users/crazy-max/subscriptions",
+        "type": "User",
+        "url": "https://api.github.com/users/crazy-max",
+        "user_view_type": "public"
+      }
+    },
+    "html_url": "https://github.com/docker/test-docker-action/pull/55",
+    "id": 2943619700,
+    "issue_url": "https://api.github.com/repos/docker/test-docker-action/issues/55",
+    "labels": [],
+    "locked": false,
+    "maintainer_can_modify": true,
+    "merge_commit_sha": "185f792b68ccf87b60a6260f08f247c8fa209488",
+    "mergeable": null,
+    "mergeable_state": "unknown",
+    "merged": false,
+    "merged_at": null,
+    "merged_by": null,
+    "milestone": null,
+    "node_id": "PR_kwDOEQjNNc6vdBJ0",
+    "number": 55,
+    "patch_url": "https://github.com/docker/test-docker-action/pull/55.patch",
+    "rebaseable": null,
+    "requested_reviewers": [],
+    "requested_teams": [],
+    "review_comment_url": "https://api.github.com/repos/docker/test-docker-action/pulls/comments{/number}",
+    "review_comments": 0,
+    "review_comments_url": "https://api.github.com/repos/docker/test-docker-action/pulls/55/comments",
+    "state": "open",
+    "statuses_url": "https://api.github.com/repos/docker/test-docker-action/statuses/3be79a9f0a68f4a7b93c001d377d9397663f6a5f",
+    "title": "ghutil",
+    "updated_at": "2025-10-24T08:27:14Z",
+    "url": "https://api.github.com/repos/docker/test-docker-action/pulls/55",
+    "user": {
+      "avatar_url": "https://avatars.githubusercontent.com/u/1951866?v=4",
+      "events_url": "https://api.github.com/users/crazy-max/events{/privacy}",
+      "followers_url": "https://api.github.com/users/crazy-max/followers",
+      "following_url": "https://api.github.com/users/crazy-max/following{/other_user}",
+      "gists_url": "https://api.github.com/users/crazy-max/gists{/gist_id}",
+      "gravatar_id": "",
+      "html_url": "https://github.com/crazy-max",
+      "id": 1951866,
+      "login": "crazy-max",
+      "node_id": "MDQ6VXNlcjE5NTE4NjY=",
+      "organizations_url": "https://api.github.com/users/crazy-max/orgs",
+      "received_events_url": "https://api.github.com/users/crazy-max/received_events",
+      "repos_url": "https://api.github.com/users/crazy-max/repos",
+      "site_admin": false,
+      "starred_url": "https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/crazy-max/subscriptions",
+      "type": "User",
+      "url": "https://api.github.com/users/crazy-max",
+      "user_view_type": "public"
+    }
+  },
+  "repository": {
+    "allow_forking": true,
+    "archive_url": "https://api.github.com/repos/docker/test-docker-action/{archive_format}{/ref}",
+    "archived": false,
+    "assignees_url": "https://api.github.com/repos/docker/test-docker-action/assignees{/user}",
+    "blobs_url": "https://api.github.com/repos/docker/test-docker-action/git/blobs{/sha}",
+    "branches_url": "https://api.github.com/repos/docker/test-docker-action/branches{/branch}",
+    "clone_url": "https://github.com/docker/test-docker-action.git",
+    "collaborators_url": "https://api.github.com/repos/docker/test-docker-action/collaborators{/collaborator}",
+    "comments_url": "https://api.github.com/repos/docker/test-docker-action/comments{/number}",
+    "commits_url": "https://api.github.com/repos/docker/test-docker-action/commits{/sha}",
+    "compare_url": "https://api.github.com/repos/docker/test-docker-action/compare/{base}...{head}",
+    "contents_url": "https://api.github.com/repos/docker/test-docker-action/contents/{+path}",
+    "contributors_url": "https://api.github.com/repos/docker/test-docker-action/contributors",
+    "created_at": "2020-08-07T09:23:00Z",
+    "custom_properties": {},
+    "default_branch": "master",
+    "deployments_url": "https://api.github.com/repos/docker/test-docker-action/deployments",
+    "description": "Test \"Docker\" Actions",
+    "disabled": false,
+    "downloads_url": "https://api.github.com/repos/docker/test-docker-action/downloads",
+    "events_url": "https://api.github.com/repos/docker/test-docker-action/events",
+    "fork": false,
+    "forks": 1,
+    "forks_count": 1,
+    "forks_url": "https://api.github.com/repos/docker/test-docker-action/forks",
+    "full_name": "docker/test-docker-action",
+    "git_commits_url": "https://api.github.com/repos/docker/test-docker-action/git/commits{/sha}",
+    "git_refs_url": "https://api.github.com/repos/docker/test-docker-action/git/refs{/sha}",
+    "git_tags_url": "https://api.github.com/repos/docker/test-docker-action/git/tags{/sha}",
+    "git_url": "git://github.com/docker/test-docker-action.git",
+    "has_discussions": false,
+    "has_downloads": true,
+    "has_issues": true,
+    "has_pages": false,
+    "has_projects": false,
+    "has_wiki": false,
+    "homepage": "",
+    "hooks_url": "https://api.github.com/repos/docker/test-docker-action/hooks",
+    "html_url": "https://github.com/docker/test-docker-action",
+    "id": 285789493,
+    "is_template": false,
+    "issue_comment_url": "https://api.github.com/repos/docker/test-docker-action/issues/comments{/number}",
+    "issue_events_url": "https://api.github.com/repos/docker/test-docker-action/issues/events{/number}",
+    "issues_url": "https://api.github.com/repos/docker/test-docker-action/issues{/number}",
+    "keys_url": "https://api.github.com/repos/docker/test-docker-action/keys{/key_id}",
+    "labels_url": "https://api.github.com/repos/docker/test-docker-action/labels{/name}",
+    "language": "JavaScript",
+    "languages_url": "https://api.github.com/repos/docker/test-docker-action/languages",
+    "license": {
+      "key": "mit",
+      "name": "MIT License",
+      "node_id": "MDc6TGljZW5zZTEz",
+      "spdx_id": "MIT",
+      "url": "https://api.github.com/licenses/mit"
+    },
+    "merges_url": "https://api.github.com/repos/docker/test-docker-action/merges",
+    "milestones_url": "https://api.github.com/repos/docker/test-docker-action/milestones{/number}",
+    "mirror_url": null,
+    "name": "test-docker-action",
+    "node_id": "MDEwOlJlcG9zaXRvcnkyODU3ODk0OTM=",
+    "notifications_url": "https://api.github.com/repos/docker/test-docker-action/notifications{?since,all,participating}",
+    "open_issues": 16,
+    "open_issues_count": 16,
+    "owner": {
+      "avatar_url": "https://avatars.githubusercontent.com/u/5429470?v=4",
+      "events_url": "https://api.github.com/users/docker/events{/privacy}",
+      "followers_url": "https://api.github.com/users/docker/followers",
+      "following_url": "https://api.github.com/users/docker/following{/other_user}",
+      "gists_url": "https://api.github.com/users/docker/gists{/gist_id}",
+      "gravatar_id": "",
+      "html_url": "https://github.com/docker",
+      "id": 5429470,
+      "login": "docker",
+      "node_id": "MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+      "organizations_url": "https://api.github.com/users/docker/orgs",
+      "received_events_url": "https://api.github.com/users/docker/received_events",
+      "repos_url": "https://api.github.com/users/docker/repos",
+      "site_admin": false,
+      "starred_url": "https://api.github.com/users/docker/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/docker/subscriptions",
+      "type": "Organization",
+      "url": "https://api.github.com/users/docker",
+      "user_view_type": "public"
+    },
+    "private": true,
+    "pulls_url": "https://api.github.com/repos/docker/test-docker-action/pulls{/number}",
+    "pushed_at": "2025-09-29T14:04:23Z",
+    "releases_url": "https://api.github.com/repos/docker/test-docker-action/releases{/id}",
+    "size": 1374,
+    "ssh_url": "git@github.com:docker/test-docker-action.git",
+    "stargazers_count": 1,
+    "stargazers_url": "https://api.github.com/repos/docker/test-docker-action/stargazers",
+    "statuses_url": "https://api.github.com/repos/docker/test-docker-action/statuses/{sha}",
+    "subscribers_url": "https://api.github.com/repos/docker/test-docker-action/subscribers",
+    "subscription_url": "https://api.github.com/repos/docker/test-docker-action/subscription",
+    "svn_url": "https://github.com/docker/test-docker-action",
+    "tags_url": "https://api.github.com/repos/docker/test-docker-action/tags",
+    "teams_url": "https://api.github.com/repos/docker/test-docker-action/teams",
+    "topics": [],
+    "trees_url": "https://api.github.com/repos/docker/test-docker-action/git/trees{/sha}",
+    "updated_at": "2025-09-29T14:04:27Z",
+    "url": "https://api.github.com/repos/docker/test-docker-action",
+    "visibility": "internal",
+    "watchers": 1,
+    "watchers_count": 1,
+    "web_commit_signoff_required": false
+  },
+  "sender": {
+    "avatar_url": "https://avatars.githubusercontent.com/u/1951866?v=4",
+    "events_url": "https://api.github.com/users/crazy-max/events{/privacy}",
+    "followers_url": "https://api.github.com/users/crazy-max/followers",
+    "following_url": "https://api.github.com/users/crazy-max/following{/other_user}",
+    "gists_url": "https://api.github.com/users/crazy-max/gists{/gist_id}",
+    "gravatar_id": "",
+    "html_url": "https://github.com/crazy-max",
+    "id": 1951866,
+    "login": "crazy-max",
+    "node_id": "MDQ6VXNlcjE5NTE4NjY=",
+    "organizations_url": "https://api.github.com/users/crazy-max/orgs",
+    "received_events_url": "https://api.github.com/users/crazy-max/received_events",
+    "repos_url": "https://api.github.com/users/crazy-max/repos",
+    "site_admin": false,
+    "starred_url": "https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/crazy-max/subscriptions",
+    "type": "User",
+    "url": "https://api.github.com/users/crazy-max",
+    "user_view_type": "public"
+  }
+}
\ No newline at end of file
diff --git a/util/ghutil/fixtures/ghactx_pr_expected.json b/util/ghutil/fixtures/ghactx_pr_expected.json
new file mode 100644
index 00000000..a8b5b92c
--- /dev/null
+++ b/util/ghutil/fixtures/ghactx_pr_expected.json
@@ -0,0 +1,587 @@
+{
+  "github_actor":"crazy-max",
+  "github_actor_id":"1951866",
+  "github_event_name":"pull_request",
+  "github_event_payload":{
+    "action":"synchronize",
+    "after":"3be79a9f0a68f4a7b93c001d377d9397663f6a5f",
+    "before":"c89d99c85a2603825bf11a688a446cfb50488deb",
+    "enterprise":{
+      "avatar_url":"https://avatars.githubusercontent.com/b/19176?v=4",
+      "created_at":"2022-12-30T23:53:17Z",
+      "description":null,
+      "html_url":"https://github.com/enterprises/docker",
+      "id":19176,
+      "name":"Docker",
+      "node_id":"E_kgDNSug",
+      "slug":"docker",
+      "updated_at":"2025-10-20T20:39:05Z",
+      "website_url":null
+    },
+    "number":55,
+    "organization":{
+      "avatar_url":"https://avatars.githubusercontent.com/u/5429470?v=4",
+      "description":"Docker helps developers bring their ideas to life by conquering the complexity of app development.",
+      "events_url":"https://api.github.com/orgs/docker/events",
+      "hooks_url":"https://api.github.com/orgs/docker/hooks",
+      "id":5429470,
+      "issues_url":"https://api.github.com/orgs/docker/issues",
+      "login":"docker",
+      "members_url":"https://api.github.com/orgs/docker/members{/member}",
+      "node_id":"MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+      "public_members_url":"https://api.github.com/orgs/docker/public_members{/member}",
+      "repos_url":"https://api.github.com/orgs/docker/repos",
+      "url":"https://api.github.com/orgs/docker"
+    },
+    "pull_request":{
+      "_links":{
+        "comments":{
+          "href":"https://api.github.com/repos/docker/test-docker-action/issues/55/comments"
+        },
+        "commits":{
+          "href":"https://api.github.com/repos/docker/test-docker-action/pulls/55/commits"
+        },
+        "html":{
+          "href":"https://github.com/docker/test-docker-action/pull/55"
+        },
+        "issue":{
+          "href":"https://api.github.com/repos/docker/test-docker-action/issues/55"
+        },
+        "review_comment":{
+          "href":"https://api.github.com/repos/docker/test-docker-action/pulls/comments{/number}"
+        },
+        "review_comments":{
+          "href":"https://api.github.com/repos/docker/test-docker-action/pulls/55/comments"
+        },
+        "self":{
+          "href":"https://api.github.com/repos/docker/test-docker-action/pulls/55"
+        },
+        "statuses":{
+          "href":"https://api.github.com/repos/docker/test-docker-action/statuses/3be79a9f0a68f4a7b93c001d377d9397663f6a5f"
+        }
+      },
+      "active_lock_reason":null,
+      "additions":25,
+      "assignee":null,
+      "assignees":[
+
+      ],
+      "author_association":"MEMBER",
+      "auto_merge":null,
+      "base":{
+        "label":"docker:master",
+        "ref":"master",
+        "repo":{
+          "allow_auto_merge":false,
+          "allow_forking":true,
+          "allow_merge_commit":true,
+          "allow_rebase_merge":true,
+          "allow_squash_merge":true,
+          "allow_update_branch":false,
+          "archive_url":"https://api.github.com/repos/docker/test-docker-action/{archive_format}{/ref}",
+          "archived":false,
+          "assignees_url":"https://api.github.com/repos/docker/test-docker-action/assignees{/user}",
+          "blobs_url":"https://api.github.com/repos/docker/test-docker-action/git/blobs{/sha}",
+          "branches_url":"https://api.github.com/repos/docker/test-docker-action/branches{/branch}",
+          "clone_url":"https://github.com/docker/test-docker-action.git",
+          "collaborators_url":"https://api.github.com/repos/docker/test-docker-action/collaborators{/collaborator}",
+          "comments_url":"https://api.github.com/repos/docker/test-docker-action/comments{/number}",
+          "commits_url":"https://api.github.com/repos/docker/test-docker-action/commits{/sha}",
+          "compare_url":"https://api.github.com/repos/docker/test-docker-action/compare/{base}...{head}",
+          "contents_url":"https://api.github.com/repos/docker/test-docker-action/contents/{+path}",
+          "contributors_url":"https://api.github.com/repos/docker/test-docker-action/contributors",
+          "created_at":"2020-08-07T09:23:00Z",
+          "default_branch":"master",
+          "delete_branch_on_merge":false,
+          "deployments_url":"https://api.github.com/repos/docker/test-docker-action/deployments",
+          "description":"Test \"Docker\" Actions",
+          "disabled":false,
+          "downloads_url":"https://api.github.com/repos/docker/test-docker-action/downloads",
+          "events_url":"https://api.github.com/repos/docker/test-docker-action/events",
+          "fork":false,
+          "forks":1,
+          "forks_count":1,
+          "forks_url":"https://api.github.com/repos/docker/test-docker-action/forks",
+          "full_name":"docker/test-docker-action",
+          "git_commits_url":"https://api.github.com/repos/docker/test-docker-action/git/commits{/sha}",
+          "git_refs_url":"https://api.github.com/repos/docker/test-docker-action/git/refs{/sha}",
+          "git_tags_url":"https://api.github.com/repos/docker/test-docker-action/git/tags{/sha}",
+          "git_url":"git://github.com/docker/test-docker-action.git",
+          "has_discussions":false,
+          "has_downloads":true,
+          "has_issues":true,
+          "has_pages":false,
+          "has_projects":false,
+          "has_wiki":false,
+          "homepage":"",
+          "hooks_url":"https://api.github.com/repos/docker/test-docker-action/hooks",
+          "html_url":"https://github.com/docker/test-docker-action",
+          "id":285789493,
+          "is_template":false,
+          "issue_comment_url":"https://api.github.com/repos/docker/test-docker-action/issues/comments{/number}",
+          "issue_events_url":"https://api.github.com/repos/docker/test-docker-action/issues/events{/number}",
+          "issues_url":"https://api.github.com/repos/docker/test-docker-action/issues{/number}",
+          "keys_url":"https://api.github.com/repos/docker/test-docker-action/keys{/key_id}",
+          "labels_url":"https://api.github.com/repos/docker/test-docker-action/labels{/name}",
+          "language":"JavaScript",
+          "languages_url":"https://api.github.com/repos/docker/test-docker-action/languages",
+          "license":{
+            "key":"mit",
+            "name":"MIT License",
+            "node_id":"MDc6TGljZW5zZTEz",
+            "spdx_id":"MIT",
+            "url":"https://api.github.com/licenses/mit"
+          },
+          "merge_commit_message":"PR_TITLE",
+          "merge_commit_title":"MERGE_MESSAGE",
+          "merges_url":"https://api.github.com/repos/docker/test-docker-action/merges",
+          "milestones_url":"https://api.github.com/repos/docker/test-docker-action/milestones{/number}",
+          "mirror_url":null,
+          "name":"test-docker-action",
+          "node_id":"MDEwOlJlcG9zaXRvcnkyODU3ODk0OTM=",
+          "notifications_url":"https://api.github.com/repos/docker/test-docker-action/notifications{?since,all,participating}",
+          "open_issues":16,
+          "open_issues_count":16,
+          "owner":{
+            "avatar_url":"https://avatars.githubusercontent.com/u/5429470?v=4",
+            "events_url":"https://api.github.com/users/docker/events{/privacy}",
+            "followers_url":"https://api.github.com/users/docker/followers",
+            "following_url":"https://api.github.com/users/docker/following{/other_user}",
+            "gists_url":"https://api.github.com/users/docker/gists{/gist_id}",
+            "gravatar_id":"",
+            "html_url":"https://github.com/docker",
+            "id":5429470,
+            "login":"docker",
+            "node_id":"MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+            "organizations_url":"https://api.github.com/users/docker/orgs",
+            "received_events_url":"https://api.github.com/users/docker/received_events",
+            "repos_url":"https://api.github.com/users/docker/repos",
+            "site_admin":false,
+            "starred_url":"https://api.github.com/users/docker/starred{/owner}{/repo}",
+            "subscriptions_url":"https://api.github.com/users/docker/subscriptions",
+            "type":"Organization",
+            "url":"https://api.github.com/users/docker",
+            "user_view_type":"public"
+          },
+          "private":true,
+          "pulls_url":"https://api.github.com/repos/docker/test-docker-action/pulls{/number}",
+          "pushed_at":"2025-09-29T14:04:23Z",
+          "releases_url":"https://api.github.com/repos/docker/test-docker-action/releases{/id}",
+          "size":1374,
+          "squash_merge_commit_message":"COMMIT_MESSAGES",
+          "squash_merge_commit_title":"COMMIT_OR_PR_TITLE",
+          "ssh_url":"git@github.com:docker/test-docker-action.git",
+          "stargazers_count":1,
+          "stargazers_url":"https://api.github.com/repos/docker/test-docker-action/stargazers",
+          "statuses_url":"https://api.github.com/repos/docker/test-docker-action/statuses/{sha}",
+          "subscribers_url":"https://api.github.com/repos/docker/test-docker-action/subscribers",
+          "subscription_url":"https://api.github.com/repos/docker/test-docker-action/subscription",
+          "svn_url":"https://github.com/docker/test-docker-action",
+          "tags_url":"https://api.github.com/repos/docker/test-docker-action/tags",
+          "teams_url":"https://api.github.com/repos/docker/test-docker-action/teams",
+          "topics":[
+
+          ],
+          "trees_url":"https://api.github.com/repos/docker/test-docker-action/git/trees{/sha}",
+          "updated_at":"2025-09-29T14:04:27Z",
+          "url":"https://api.github.com/repos/docker/test-docker-action",
+          "use_squash_pr_title_as_default":false,
+          "visibility":"internal",
+          "watchers":1,
+          "watchers_count":1,
+          "web_commit_signoff_required":false
+        },
+        "sha":"231cbfa88dca156930d8e7991b6cf57bc09fd090",
+        "user":{
+          "avatar_url":"https://avatars.githubusercontent.com/u/5429470?v=4",
+          "events_url":"https://api.github.com/users/docker/events{/privacy}",
+          "followers_url":"https://api.github.com/users/docker/followers",
+          "following_url":"https://api.github.com/users/docker/following{/other_user}",
+          "gists_url":"https://api.github.com/users/docker/gists{/gist_id}",
+          "gravatar_id":"",
+          "html_url":"https://github.com/docker",
+          "id":5429470,
+          "login":"docker",
+          "node_id":"MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+          "organizations_url":"https://api.github.com/users/docker/orgs",
+          "received_events_url":"https://api.github.com/users/docker/received_events",
+          "repos_url":"https://api.github.com/users/docker/repos",
+          "site_admin":false,
+          "starred_url":"https://api.github.com/users/docker/starred{/owner}{/repo}",
+          "subscriptions_url":"https://api.github.com/users/docker/subscriptions",
+          "type":"Organization",
+          "url":"https://api.github.com/users/docker",
+          "user_view_type":"public"
+        }
+      },
+      "body":null,
+      "changed_files":1,
+      "closed_at":null,
+      "comments":0,
+      "comments_url":"https://api.github.com/repos/docker/test-docker-action/issues/55/comments",
+      "commits":1,
+      "commits_url":"https://api.github.com/repos/docker/test-docker-action/pulls/55/commits",
+      "created_at":"2025-10-24T08:25:26Z",
+      "deletions":0,
+      "diff_url":"https://github.com/docker/test-docker-action/pull/55.diff",
+      "draft":false,
+      "head":{
+        "label":"crazy-max:ghutil",
+        "ref":"ghutil",
+        "repo":{
+          "allow_auto_merge":false,
+          "allow_forking":true,
+          "allow_merge_commit":true,
+          "allow_rebase_merge":true,
+          "allow_squash_merge":true,
+          "allow_update_branch":false,
+          "archive_url":"https://api.github.com/repos/crazy-max/test-docker-action/{archive_format}{/ref}",
+          "archived":false,
+          "assignees_url":"https://api.github.com/repos/crazy-max/test-docker-action/assignees{/user}",
+          "blobs_url":"https://api.github.com/repos/crazy-max/test-docker-action/git/blobs{/sha}",
+          "branches_url":"https://api.github.com/repos/crazy-max/test-docker-action/branches{/branch}",
+          "clone_url":"https://github.com/crazy-max/test-docker-action.git",
+          "collaborators_url":"https://api.github.com/repos/crazy-max/test-docker-action/collaborators{/collaborator}",
+          "comments_url":"https://api.github.com/repos/crazy-max/test-docker-action/comments{/number}",
+          "commits_url":"https://api.github.com/repos/crazy-max/test-docker-action/commits{/sha}",
+          "compare_url":"https://api.github.com/repos/crazy-max/test-docker-action/compare/{base}...{head}",
+          "contents_url":"https://api.github.com/repos/crazy-max/test-docker-action/contents/{+path}",
+          "contributors_url":"https://api.github.com/repos/crazy-max/test-docker-action/contributors",
+          "created_at":"2021-07-11T17:34:22Z",
+          "default_branch":"master",
+          "delete_branch_on_merge":false,
+          "deployments_url":"https://api.github.com/repos/crazy-max/test-docker-action/deployments",
+          "description":"Test \"Docker\" Actions",
+          "disabled":false,
+          "downloads_url":"https://api.github.com/repos/crazy-max/test-docker-action/downloads",
+          "events_url":"https://api.github.com/repos/crazy-max/test-docker-action/events",
+          "fork":true,
+          "forks":0,
+          "forks_count":0,
+          "forks_url":"https://api.github.com/repos/crazy-max/test-docker-action/forks",
+          "full_name":"crazy-max/test-docker-action",
+          "git_commits_url":"https://api.github.com/repos/crazy-max/test-docker-action/git/commits{/sha}",
+          "git_refs_url":"https://api.github.com/repos/crazy-max/test-docker-action/git/refs{/sha}",
+          "git_tags_url":"https://api.github.com/repos/crazy-max/test-docker-action/git/tags{/sha}",
+          "git_url":"git://github.com/crazy-max/test-docker-action.git",
+          "has_discussions":false,
+          "has_downloads":true,
+          "has_issues":false,
+          "has_pages":false,
+          "has_projects":true,
+          "has_wiki":false,
+          "homepage":"",
+          "hooks_url":"https://api.github.com/repos/crazy-max/test-docker-action/hooks",
+          "html_url":"https://github.com/crazy-max/test-docker-action",
+          "id":385013169,
+          "is_template":false,
+          "issue_comment_url":"https://api.github.com/repos/crazy-max/test-docker-action/issues/comments{/number}",
+          "issue_events_url":"https://api.github.com/repos/crazy-max/test-docker-action/issues/events{/number}",
+          "issues_url":"https://api.github.com/repos/crazy-max/test-docker-action/issues{/number}",
+          "keys_url":"https://api.github.com/repos/crazy-max/test-docker-action/keys{/key_id}",
+          "labels_url":"https://api.github.com/repos/crazy-max/test-docker-action/labels{/name}",
+          "language":"JavaScript",
+          "languages_url":"https://api.github.com/repos/crazy-max/test-docker-action/languages",
+          "license":{
+            "key":"mit",
+            "name":"MIT License",
+            "node_id":"MDc6TGljZW5zZTEz",
+            "spdx_id":"MIT",
+            "url":"https://api.github.com/licenses/mit"
+          },
+          "merge_commit_message":"PR_TITLE",
+          "merge_commit_title":"MERGE_MESSAGE",
+          "merges_url":"https://api.github.com/repos/crazy-max/test-docker-action/merges",
+          "milestones_url":"https://api.github.com/repos/crazy-max/test-docker-action/milestones{/number}",
+          "mirror_url":null,
+          "name":"test-docker-action",
+          "node_id":"MDEwOlJlcG9zaXRvcnkzODUwMTMxNjk=",
+          "notifications_url":"https://api.github.com/repos/crazy-max/test-docker-action/notifications{?since,all,participating}",
+          "open_issues":0,
+          "open_issues_count":0,
+          "owner":{
+            "avatar_url":"https://avatars.githubusercontent.com/u/1951866?v=4",
+            "events_url":"https://api.github.com/users/crazy-max/events{/privacy}",
+            "followers_url":"https://api.github.com/users/crazy-max/followers",
+            "following_url":"https://api.github.com/users/crazy-max/following{/other_user}",
+            "gists_url":"https://api.github.com/users/crazy-max/gists{/gist_id}",
+            "gravatar_id":"",
+            "html_url":"https://github.com/crazy-max",
+            "id":1951866,
+            "login":"crazy-max",
+            "node_id":"MDQ6VXNlcjE5NTE4NjY=",
+            "organizations_url":"https://api.github.com/users/crazy-max/orgs",
+            "received_events_url":"https://api.github.com/users/crazy-max/received_events",
+            "repos_url":"https://api.github.com/users/crazy-max/repos",
+            "site_admin":false,
+            "starred_url":"https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
+            "subscriptions_url":"https://api.github.com/users/crazy-max/subscriptions",
+            "type":"User",
+            "url":"https://api.github.com/users/crazy-max",
+            "user_view_type":"public"
+          },
+          "private":true,
+          "pulls_url":"https://api.github.com/repos/crazy-max/test-docker-action/pulls{/number}",
+          "pushed_at":"2025-10-24T08:27:13Z",
+          "releases_url":"https://api.github.com/repos/crazy-max/test-docker-action/releases{/id}",
+          "size":1078,
+          "squash_merge_commit_message":"COMMIT_MESSAGES",
+          "squash_merge_commit_title":"COMMIT_OR_PR_TITLE",
+          "ssh_url":"git@github.com:crazy-max/test-docker-action.git",
+          "stargazers_count":0,
+          "stargazers_url":"https://api.github.com/repos/crazy-max/test-docker-action/stargazers",
+          "statuses_url":"https://api.github.com/repos/crazy-max/test-docker-action/statuses/{sha}",
+          "subscribers_url":"https://api.github.com/repos/crazy-max/test-docker-action/subscribers",
+          "subscription_url":"https://api.github.com/repos/crazy-max/test-docker-action/subscription",
+          "svn_url":"https://github.com/crazy-max/test-docker-action",
+          "tags_url":"https://api.github.com/repos/crazy-max/test-docker-action/tags",
+          "teams_url":"https://api.github.com/repos/crazy-max/test-docker-action/teams",
+          "topics":[
+
+          ],
+          "trees_url":"https://api.github.com/repos/crazy-max/test-docker-action/git/trees{/sha}",
+          "updated_at":"2025-08-12T06:56:46Z",
+          "url":"https://api.github.com/repos/crazy-max/test-docker-action",
+          "use_squash_pr_title_as_default":false,
+          "visibility":"private",
+          "watchers":0,
+          "watchers_count":0,
+          "web_commit_signoff_required":false
+        },
+        "sha":"3be79a9f0a68f4a7b93c001d377d9397663f6a5f",
+        "user":{
+          "avatar_url":"https://avatars.githubusercontent.com/u/1951866?v=4",
+          "events_url":"https://api.github.com/users/crazy-max/events{/privacy}",
+          "followers_url":"https://api.github.com/users/crazy-max/followers",
+          "following_url":"https://api.github.com/users/crazy-max/following{/other_user}",
+          "gists_url":"https://api.github.com/users/crazy-max/gists{/gist_id}",
+          "gravatar_id":"",
+          "html_url":"https://github.com/crazy-max",
+          "id":1951866,
+          "login":"crazy-max",
+          "node_id":"MDQ6VXNlcjE5NTE4NjY=",
+          "organizations_url":"https://api.github.com/users/crazy-max/orgs",
+          "received_events_url":"https://api.github.com/users/crazy-max/received_events",
+          "repos_url":"https://api.github.com/users/crazy-max/repos",
+          "site_admin":false,
+          "starred_url":"https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
+          "subscriptions_url":"https://api.github.com/users/crazy-max/subscriptions",
+          "type":"User",
+          "url":"https://api.github.com/users/crazy-max",
+          "user_view_type":"public"
+        }
+      },
+      "html_url":"https://github.com/docker/test-docker-action/pull/55",
+      "id":2943619700,
+      "issue_url":"https://api.github.com/repos/docker/test-docker-action/issues/55",
+      "labels":[
+
+      ],
+      "locked":false,
+      "maintainer_can_modify":true,
+      "merge_commit_sha":"185f792b68ccf87b60a6260f08f247c8fa209488",
+      "mergeable":null,
+      "mergeable_state":"unknown",
+      "merged":false,
+      "merged_at":null,
+      "merged_by":null,
+      "milestone":null,
+      "node_id":"PR_kwDOEQjNNc6vdBJ0",
+      "number":55,
+      "patch_url":"https://github.com/docker/test-docker-action/pull/55.patch",
+      "rebaseable":null,
+      "requested_reviewers":[
+
+      ],
+      "requested_teams":[
+
+      ],
+      "review_comment_url":"https://api.github.com/repos/docker/test-docker-action/pulls/comments{/number}",
+      "review_comments":0,
+      "review_comments_url":"https://api.github.com/repos/docker/test-docker-action/pulls/55/comments",
+      "state":"open",
+      "statuses_url":"https://api.github.com/repos/docker/test-docker-action/statuses/3be79a9f0a68f4a7b93c001d377d9397663f6a5f",
+      "title":"ghutil",
+      "updated_at":"2025-10-24T08:27:14Z",
+      "url":"https://api.github.com/repos/docker/test-docker-action/pulls/55",
+      "user":{
+        "avatar_url":"https://avatars.githubusercontent.com/u/1951866?v=4",
+        "events_url":"https://api.github.com/users/crazy-max/events{/privacy}",
+        "followers_url":"https://api.github.com/users/crazy-max/followers",
+        "following_url":"https://api.github.com/users/crazy-max/following{/other_user}",
+        "gists_url":"https://api.github.com/users/crazy-max/gists{/gist_id}",
+        "gravatar_id":"",
+        "html_url":"https://github.com/crazy-max",
+        "id":1951866,
+        "login":"crazy-max",
+        "node_id":"MDQ6VXNlcjE5NTE4NjY=",
+        "organizations_url":"https://api.github.com/users/crazy-max/orgs",
+        "received_events_url":"https://api.github.com/users/crazy-max/received_events",
+        "repos_url":"https://api.github.com/users/crazy-max/repos",
+        "site_admin":false,
+        "starred_url":"https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
+        "subscriptions_url":"https://api.github.com/users/crazy-max/subscriptions",
+        "type":"User",
+        "url":"https://api.github.com/users/crazy-max",
+        "user_view_type":"public"
+      }
+    },
+    "repository":{
+      "allow_forking":true,
+      "archive_url":"https://api.github.com/repos/docker/test-docker-action/{archive_format}{/ref}",
+      "archived":false,
+      "assignees_url":"https://api.github.com/repos/docker/test-docker-action/assignees{/user}",
+      "blobs_url":"https://api.github.com/repos/docker/test-docker-action/git/blobs{/sha}",
+      "branches_url":"https://api.github.com/repos/docker/test-docker-action/branches{/branch}",
+      "clone_url":"https://github.com/docker/test-docker-action.git",
+      "collaborators_url":"https://api.github.com/repos/docker/test-docker-action/collaborators{/collaborator}",
+      "comments_url":"https://api.github.com/repos/docker/test-docker-action/comments{/number}",
+      "commits_url":"https://api.github.com/repos/docker/test-docker-action/commits{/sha}",
+      "compare_url":"https://api.github.com/repos/docker/test-docker-action/compare/{base}...{head}",
+      "contents_url":"https://api.github.com/repos/docker/test-docker-action/contents/{+path}",
+      "contributors_url":"https://api.github.com/repos/docker/test-docker-action/contributors",
+      "created_at":"2020-08-07T09:23:00Z",
+      "custom_properties":{
+
+      },
+      "default_branch":"master",
+      "deployments_url":"https://api.github.com/repos/docker/test-docker-action/deployments",
+      "description":"Test \"Docker\" Actions",
+      "disabled":false,
+      "downloads_url":"https://api.github.com/repos/docker/test-docker-action/downloads",
+      "events_url":"https://api.github.com/repos/docker/test-docker-action/events",
+      "fork":false,
+      "forks":1,
+      "forks_count":1,
+      "forks_url":"https://api.github.com/repos/docker/test-docker-action/forks",
+      "full_name":"docker/test-docker-action",
+      "git_commits_url":"https://api.github.com/repos/docker/test-docker-action/git/commits{/sha}",
+      "git_refs_url":"https://api.github.com/repos/docker/test-docker-action/git/refs{/sha}",
+      "git_tags_url":"https://api.github.com/repos/docker/test-docker-action/git/tags{/sha}",
+      "git_url":"git://github.com/docker/test-docker-action.git",
+      "has_discussions":false,
+      "has_downloads":true,
+      "has_issues":true,
+      "has_pages":false,
+      "has_projects":false,
+      "has_wiki":false,
+      "homepage":"",
+      "hooks_url":"https://api.github.com/repos/docker/test-docker-action/hooks",
+      "html_url":"https://github.com/docker/test-docker-action",
+      "id":285789493,
+      "is_template":false,
+      "issue_comment_url":"https://api.github.com/repos/docker/test-docker-action/issues/comments{/number}",
+      "issue_events_url":"https://api.github.com/repos/docker/test-docker-action/issues/events{/number}",
+      "issues_url":"https://api.github.com/repos/docker/test-docker-action/issues{/number}",
+      "keys_url":"https://api.github.com/repos/docker/test-docker-action/keys{/key_id}",
+      "labels_url":"https://api.github.com/repos/docker/test-docker-action/labels{/name}",
+      "language":"JavaScript",
+      "languages_url":"https://api.github.com/repos/docker/test-docker-action/languages",
+      "license":{
+        "key":"mit",
+        "name":"MIT License",
+        "node_id":"MDc6TGljZW5zZTEz",
+        "spdx_id":"MIT",
+        "url":"https://api.github.com/licenses/mit"
+      },
+      "merges_url":"https://api.github.com/repos/docker/test-docker-action/merges",
+      "milestones_url":"https://api.github.com/repos/docker/test-docker-action/milestones{/number}",
+      "mirror_url":null,
+      "name":"test-docker-action",
+      "node_id":"MDEwOlJlcG9zaXRvcnkyODU3ODk0OTM=",
+      "notifications_url":"https://api.github.com/repos/docker/test-docker-action/notifications{?since,all,participating}",
+      "open_issues":16,
+      "open_issues_count":16,
+      "owner":{
+        "avatar_url":"https://avatars.githubusercontent.com/u/5429470?v=4",
+        "events_url":"https://api.github.com/users/docker/events{/privacy}",
+        "followers_url":"https://api.github.com/users/docker/followers",
+        "following_url":"https://api.github.com/users/docker/following{/other_user}",
+        "gists_url":"https://api.github.com/users/docker/gists{/gist_id}",
+        "gravatar_id":"",
+        "html_url":"https://github.com/docker",
+        "id":5429470,
+        "login":"docker",
+        "node_id":"MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+        "organizations_url":"https://api.github.com/users/docker/orgs",
+        "received_events_url":"https://api.github.com/users/docker/received_events",
+        "repos_url":"https://api.github.com/users/docker/repos",
+        "site_admin":false,
+        "starred_url":"https://api.github.com/users/docker/starred{/owner}{/repo}",
+        "subscriptions_url":"https://api.github.com/users/docker/subscriptions",
+        "type":"Organization",
+        "url":"https://api.github.com/users/docker",
+        "user_view_type":"public"
+      },
+      "private":true,
+      "pulls_url":"https://api.github.com/repos/docker/test-docker-action/pulls{/number}",
+      "pushed_at":"2025-09-29T14:04:23Z",
+      "releases_url":"https://api.github.com/repos/docker/test-docker-action/releases{/id}",
+      "size":1374,
+      "ssh_url":"git@github.com:docker/test-docker-action.git",
+      "stargazers_count":1,
+      "stargazers_url":"https://api.github.com/repos/docker/test-docker-action/stargazers",
+      "statuses_url":"https://api.github.com/repos/docker/test-docker-action/statuses/{sha}",
+      "subscribers_url":"https://api.github.com/repos/docker/test-docker-action/subscribers",
+      "subscription_url":"https://api.github.com/repos/docker/test-docker-action/subscription",
+      "svn_url":"https://github.com/docker/test-docker-action",
+      "tags_url":"https://api.github.com/repos/docker/test-docker-action/tags",
+      "teams_url":"https://api.github.com/repos/docker/test-docker-action/teams",
+      "topics":[
+
+      ],
+      "trees_url":"https://api.github.com/repos/docker/test-docker-action/git/trees{/sha}",
+      "updated_at":"2025-09-29T14:04:27Z",
+      "url":"https://api.github.com/repos/docker/test-docker-action",
+      "visibility":"internal",
+      "watchers":1,
+      "watchers_count":1,
+      "web_commit_signoff_required":false
+    },
+    "sender":{
+      "avatar_url":"https://avatars.githubusercontent.com/u/1951866?v=4",
+      "events_url":"https://api.github.com/users/crazy-max/events{/privacy}",
+      "followers_url":"https://api.github.com/users/crazy-max/followers",
+      "following_url":"https://api.github.com/users/crazy-max/following{/other_user}",
+      "gists_url":"https://api.github.com/users/crazy-max/gists{/gist_id}",
+      "gravatar_id":"",
+      "html_url":"https://github.com/crazy-max",
+      "id":1951866,
+      "login":"crazy-max",
+      "node_id":"MDQ6VXNlcjE5NTE4NjY=",
+      "organizations_url":"https://api.github.com/users/crazy-max/orgs",
+      "received_events_url":"https://api.github.com/users/crazy-max/received_events",
+      "repos_url":"https://api.github.com/users/crazy-max/repos",
+      "site_admin":false,
+      "starred_url":"https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
+      "subscriptions_url":"https://api.github.com/users/crazy-max/subscriptions",
+      "type":"User",
+      "url":"https://api.github.com/users/crazy-max",
+      "user_view_type":"public"
+    }
+  },
+  "github_job":"ghutil",
+  "github_ref":"refs/pull/55/merge",
+  "github_ref_name":"55/merge",
+  "github_ref_protected":"false",
+  "github_ref_type":"branch",
+  "github_repository":"docker/test-docker-action",
+  "github_repository_id":"285789493",
+  "github_repository_owner":"docker",
+  "github_repository_owner_id":"5429470",
+  "github_run_attempt":"1",
+  "github_run_id":"18774179609",
+  "github_run_number":"2",
+  "github_runner_arch":"X64",
+  "github_runner_environment":"github-hosted",
+  "github_runner_image_os":"ubuntu24",
+  "github_runner_image_version":"20250929.60.1",
+  "github_runner_name":"GitHub Actions 1002237010",
+  "github_runner_os":"Linux",
+  "github_runner_tracking_id":"github_aef5ac41-9cf7-4195-9da9-851d8b29e68e",
+  "github_server_url":"https://github.com",
+  "github_triggering_actor":"crazy-max",
+  "github_workflow":"ghutil",
+  "github_workflow_ref":"docker/test-docker-action/.github/workflows/ghutil.yml@refs/pull/55/merge",
+  "github_workflow_sha":"198c644a687204e604c8476ad23d7d5bbb0dc221"
+}
\ No newline at end of file
diff --git a/util/ghutil/fixtures/ghactx_push.env b/util/ghutil/fixtures/ghactx_push.env
new file mode 100644
index 00000000..56d989cc
--- /dev/null
+++ b/util/ghutil/fixtures/ghactx_push.env
@@ -0,0 +1,106 @@
+ACCEPT_EULA=Y
+ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE=/opt/actionarchivecache
+AGENT_TOOLSDIRECTORY=/opt/hostedtoolcache
+ANDROID_HOME=/usr/local/lib/android/sdk
+ANDROID_NDK=/usr/local/lib/android/sdk/ndk/27.3.13750724
+ANDROID_NDK_HOME=/usr/local/lib/android/sdk/ndk/27.3.13750724
+ANDROID_NDK_LATEST_HOME=/usr/local/lib/android/sdk/ndk/28.2.13676358
+ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/27.3.13750724
+ANDROID_SDK_ROOT=/usr/local/lib/android/sdk
+ANT_HOME=/usr/share/ant
+AZURE_EXTENSION_DIR=/opt/az/azcliextensions
+BOOTSTRAP_HASKELL_NONINTERACTIVE=1
+CHROMEWEBDRIVER=/usr/local/share/chromedriver-linux64
+CHROME_BIN=/usr/bin/google-chrome
+CI=true
+CONDA=/usr/share/miniconda
+DEBIAN_FRONTEND=noninteractive
+DOTNET_MULTILEVEL_LOOKUP=0
+DOTNET_NOLOGO=1
+DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
+EDGEWEBDRIVER=/usr/local/share/edge_driver
+ENABLE_RUNNER_TRACING=true
+GECKOWEBDRIVER=/usr/local/share/gecko_driver
+GHCUP_INSTALL_BASE_PREFIX=/usr/local
+GITHUB_ACTION=__run
+GITHUB_ACTIONS=true
+GITHUB_ACTION_REF=
+GITHUB_ACTION_REPOSITORY=
+GITHUB_ACTOR=crazy-max
+GITHUB_ACTOR_ID=1951866
+GITHUB_API_URL=https://api.github.com
+GITHUB_BASE_REF=
+GITHUB_ENV=/home/runner/work/_temp/_runner_file_commands/set_env_8c7a007e-3ee1-4a13-81b8-f9a4e54c4d13
+GITHUB_EVENT_NAME=push
+GITHUB_EVENT_PATH=./fixtures/ghactx_push_event.json
+GITHUB_GRAPHQL_URL=https://api.github.com/graphql
+GITHUB_HEAD_REF=
+GITHUB_JOB=ghutil
+GITHUB_OUTPUT=/home/runner/work/_temp/_runner_file_commands/set_output_8c7a007e-3ee1-4a13-81b8-f9a4e54c4d13
+GITHUB_PATH=/home/runner/work/_temp/_runner_file_commands/add_path_8c7a007e-3ee1-4a13-81b8-f9a4e54c4d13
+GITHUB_REF=refs/heads/master
+GITHUB_REF_NAME=master
+GITHUB_REF_PROTECTED=false
+GITHUB_REF_TYPE=branch
+GITHUB_REPOSITORY=docker/test-docker-action
+GITHUB_REPOSITORY_ID=285789493
+GITHUB_REPOSITORY_OWNER=docker
+GITHUB_REPOSITORY_OWNER_ID=5429470
+GITHUB_RETENTION_DAYS=90
+GITHUB_RUN_ATTEMPT=1
+GITHUB_RUN_ID=18774637729
+GITHUB_RUN_NUMBER=3
+GITHUB_SERVER_URL=https://github.com
+GITHUB_SHA=42f57536c4a55a4723772a03ead9da2940abb3d6
+GITHUB_STATE=/home/runner/work/_temp/_runner_file_commands/save_state_8c7a007e-3ee1-4a13-81b8-f9a4e54c4d13
+GITHUB_STEP_SUMMARY=/home/runner/work/_temp/_runner_file_commands/step_summary_8c7a007e-3ee1-4a13-81b8-f9a4e54c4d13
+GITHUB_TRIGGERING_ACTOR=crazy-max
+GITHUB_WORKFLOW=ghutil
+GITHUB_WORKFLOW_REF=docker/test-docker-action/.github/workflows/ghutil.yml@refs/heads/master
+GITHUB_WORKFLOW_SHA=42f57536c4a55a4723772a03ead9da2940abb3d6
+GITHUB_WORKSPACE=/home/runner/work/test-docker-action/test-docker-action
+GOROOT_1_22_X64=/opt/hostedtoolcache/go/1.22.12/x64
+GOROOT_1_23_X64=/opt/hostedtoolcache/go/1.23.12/x64
+GOROOT_1_24_X64=/opt/hostedtoolcache/go/1.24.7/x64
+GRADLE_HOME=/usr/share/gradle-9.1.0
+HOME=/home/runner
+HOMEBREW_CLEANUP_PERIODIC_FULL_DAYS=3650
+HOMEBREW_NO_AUTO_UPDATE=1
+INVOCATION_ID=46642583e47d408d8b503e09ee054ce8
+ImageOS=ubuntu24
+ImageVersion=20250929.60.1
+JAVA_HOME=/usr/lib/jvm/temurin-17-jdk-amd64
+JAVA_HOME_11_X64=/usr/lib/jvm/temurin-11-jdk-amd64
+JAVA_HOME_17_X64=/usr/lib/jvm/temurin-17-jdk-amd64
+JAVA_HOME_21_X64=/usr/lib/jvm/temurin-21-jdk-amd64
+JAVA_HOME_25_X64=/usr/lib/jvm/temurin-25-jdk-amd64
+JAVA_HOME_8_X64=/usr/lib/jvm/temurin-8-jdk-amd64
+JOURNAL_STREAM=9:12559
+LANG=C.UTF-8
+LOGNAME=runner
+MEMORY_PRESSURE_WATCH=/sys/fs/cgroup/system.slice/hosted-compute-agent.service/memory.pressure
+MEMORY_PRESSURE_WRITE=c29tZSAyMDAwMDAgMjAwMDAwMAA=
+NVM_DIR=/home/runner/.nvm
+PATH=/snap/bin:/home/runner/.local/bin:/opt/pipx_bin:/home/runner/.cargo/bin:/home/runner/.config/composer/vendor/bin:/usr/local/.ghcup/bin:/home/runner/.dotnet/tools:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
+PIPX_BIN_DIR=/opt/pipx_bin
+PIPX_HOME=/opt/pipx
+POWERSHELL_DISTRIBUTION_CHANNEL=GitHub-Actions-ubuntu24
+RUNNER_ARCH=X64
+RUNNER_ENVIRONMENT=github-hosted
+RUNNER_NAME=GitHub Actions 1002237537
+RUNNER_OS=Linux
+RUNNER_TEMP=/home/runner/work/_temp
+RUNNER_TOOL_CACHE=/opt/hostedtoolcache
+RUNNER_TRACKING_ID=github_a790683c-a332-45df-b720-24d8d022bfc7
+RUNNER_WORKSPACE=/home/runner/work/test-docker-action
+SELENIUM_JAR_PATH=/usr/share/java/selenium-server.jar
+SGX_AESM_ADDR=1
+SHELL=/bin/bash
+SHLVL=1
+SWIFT_PATH=/usr/share/swift/usr/bin
+SYSTEMD_EXEC_PID=1751
+USER=runner
+VCPKG_INSTALLATION_ROOT=/usr/local/share/vcpkg
+XDG_CONFIG_HOME=/home/runner/.config
+XDG_RUNTIME_DIR=/run/user/1001
+_=/usr/bin/env
\ No newline at end of file
diff --git a/util/ghutil/fixtures/ghactx_push_event.json b/util/ghutil/fixtures/ghactx_push_event.json
new file mode 100644
index 00000000..eabd6f69
--- /dev/null
+++ b/util/ghutil/fixtures/ghactx_push_event.json
@@ -0,0 +1,230 @@
+{
+  "after": "42f57536c4a55a4723772a03ead9da2940abb3d6",
+  "base_ref": null,
+  "before": "231cbfa88dca156930d8e7991b6cf57bc09fd090",
+  "commits": [
+    {
+      "author": {
+        "email": "1951866+crazy-max@users.noreply.github.com",
+        "name": "CrazyMax",
+        "username": "crazy-max"
+      },
+      "committer": {
+        "email": "1951866+crazy-max@users.noreply.github.com",
+        "name": "CrazyMax",
+        "username": "crazy-max"
+      },
+      "distinct": false,
+      "id": "3be79a9f0a68f4a7b93c001d377d9397663f6a5f",
+      "message": "ghutil",
+      "timestamp": "2025-10-24T10:27:10+02:00",
+      "tree_id": "134cdd78c695edc2dacc06fa212cbe393b84b93b",
+      "url": "https://github.com/docker/test-docker-action/commit/3be79a9f0a68f4a7b93c001d377d9397663f6a5f"
+    },
+    {
+      "author": {
+        "email": "1951866+crazy-max@users.noreply.github.com",
+        "name": "CrazyMax",
+        "username": "crazy-max"
+      },
+      "committer": {
+        "email": "noreply@github.com",
+        "name": "GitHub",
+        "username": "web-flow"
+      },
+      "distinct": true,
+      "id": "42f57536c4a55a4723772a03ead9da2940abb3d6",
+      "message": "Merge pull request #55 from crazy-max/ghutil\n\nghutil",
+      "timestamp": "2025-10-24T10:46:33+02:00",
+      "tree_id": "134cdd78c695edc2dacc06fa212cbe393b84b93b",
+      "url": "https://github.com/docker/test-docker-action/commit/42f57536c4a55a4723772a03ead9da2940abb3d6"
+    }
+  ],
+  "compare": "https://github.com/docker/test-docker-action/compare/231cbfa88dca...42f57536c4a5",
+  "created": false,
+  "deleted": false,
+  "enterprise": {
+    "avatar_url": "https://avatars.githubusercontent.com/b/19176?v=4",
+    "created_at": "2022-12-30T23:53:17Z",
+    "description": null,
+    "html_url": "https://github.com/enterprises/docker",
+    "id": 19176,
+    "name": "Docker",
+    "node_id": "E_kgDNSug",
+    "slug": "docker",
+    "updated_at": "2025-10-20T20:39:05Z",
+    "website_url": null
+  },
+  "forced": false,
+  "head_commit": {
+    "author": {
+      "email": "1951866+crazy-max@users.noreply.github.com",
+      "name": "CrazyMax",
+      "username": "crazy-max"
+    },
+    "committer": {
+      "email": "noreply@github.com",
+      "name": "GitHub",
+      "username": "web-flow"
+    },
+    "distinct": true,
+    "id": "42f57536c4a55a4723772a03ead9da2940abb3d6",
+    "message": "Merge pull request #55 from crazy-max/ghutil\n\nghutil",
+    "timestamp": "2025-10-24T10:46:33+02:00",
+    "tree_id": "134cdd78c695edc2dacc06fa212cbe393b84b93b",
+    "url": "https://github.com/docker/test-docker-action/commit/42f57536c4a55a4723772a03ead9da2940abb3d6"
+  },
+  "organization": {
+    "avatar_url": "https://avatars.githubusercontent.com/u/5429470?v=4",
+    "description": "Docker helps developers bring their ideas to life by conquering the complexity of app development.",
+    "events_url": "https://api.github.com/orgs/docker/events",
+    "hooks_url": "https://api.github.com/orgs/docker/hooks",
+    "id": 5429470,
+    "issues_url": "https://api.github.com/orgs/docker/issues",
+    "login": "docker",
+    "members_url": "https://api.github.com/orgs/docker/members{/member}",
+    "node_id": "MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+    "public_members_url": "https://api.github.com/orgs/docker/public_members{/member}",
+    "repos_url": "https://api.github.com/orgs/docker/repos",
+    "url": "https://api.github.com/orgs/docker"
+  },
+  "pusher": {
+    "email": "1951866+crazy-max@users.noreply.github.com",
+    "name": "crazy-max"
+  },
+  "ref": "refs/heads/master",
+  "repository": {
+    "allow_forking": true,
+    "archive_url": "https://api.github.com/repos/docker/test-docker-action/{archive_format}{/ref}",
+    "archived": false,
+    "assignees_url": "https://api.github.com/repos/docker/test-docker-action/assignees{/user}",
+    "blobs_url": "https://api.github.com/repos/docker/test-docker-action/git/blobs{/sha}",
+    "branches_url": "https://api.github.com/repos/docker/test-docker-action/branches{/branch}",
+    "clone_url": "https://github.com/docker/test-docker-action.git",
+    "collaborators_url": "https://api.github.com/repos/docker/test-docker-action/collaborators{/collaborator}",
+    "comments_url": "https://api.github.com/repos/docker/test-docker-action/comments{/number}",
+    "commits_url": "https://api.github.com/repos/docker/test-docker-action/commits{/sha}",
+    "compare_url": "https://api.github.com/repos/docker/test-docker-action/compare/{base}...{head}",
+    "contents_url": "https://api.github.com/repos/docker/test-docker-action/contents/{+path}",
+    "contributors_url": "https://api.github.com/repos/docker/test-docker-action/contributors",
+    "created_at": 1596792180,
+    "custom_properties": {},
+    "default_branch": "master",
+    "deployments_url": "https://api.github.com/repos/docker/test-docker-action/deployments",
+    "description": "Test \"Docker\" Actions",
+    "disabled": false,
+    "downloads_url": "https://api.github.com/repos/docker/test-docker-action/downloads",
+    "events_url": "https://api.github.com/repos/docker/test-docker-action/events",
+    "fork": false,
+    "forks": 1,
+    "forks_count": 1,
+    "forks_url": "https://api.github.com/repos/docker/test-docker-action/forks",
+    "full_name": "docker/test-docker-action",
+    "git_commits_url": "https://api.github.com/repos/docker/test-docker-action/git/commits{/sha}",
+    "git_refs_url": "https://api.github.com/repos/docker/test-docker-action/git/refs{/sha}",
+    "git_tags_url": "https://api.github.com/repos/docker/test-docker-action/git/tags{/sha}",
+    "git_url": "git://github.com/docker/test-docker-action.git",
+    "has_discussions": false,
+    "has_downloads": true,
+    "has_issues": true,
+    "has_pages": false,
+    "has_projects": false,
+    "has_wiki": false,
+    "homepage": "",
+    "hooks_url": "https://api.github.com/repos/docker/test-docker-action/hooks",
+    "html_url": "https://github.com/docker/test-docker-action",
+    "id": 285789493,
+    "is_template": false,
+    "issue_comment_url": "https://api.github.com/repos/docker/test-docker-action/issues/comments{/number}",
+    "issue_events_url": "https://api.github.com/repos/docker/test-docker-action/issues/events{/number}",
+    "issues_url": "https://api.github.com/repos/docker/test-docker-action/issues{/number}",
+    "keys_url": "https://api.github.com/repos/docker/test-docker-action/keys{/key_id}",
+    "labels_url": "https://api.github.com/repos/docker/test-docker-action/labels{/name}",
+    "language": "JavaScript",
+    "languages_url": "https://api.github.com/repos/docker/test-docker-action/languages",
+    "license": {
+      "key": "mit",
+      "name": "MIT License",
+      "node_id": "MDc6TGljZW5zZTEz",
+      "spdx_id": "MIT",
+      "url": "https://api.github.com/licenses/mit"
+    },
+    "master_branch": "master",
+    "merges_url": "https://api.github.com/repos/docker/test-docker-action/merges",
+    "milestones_url": "https://api.github.com/repos/docker/test-docker-action/milestones{/number}",
+    "mirror_url": null,
+    "name": "test-docker-action",
+    "node_id": "MDEwOlJlcG9zaXRvcnkyODU3ODk0OTM=",
+    "notifications_url": "https://api.github.com/repos/docker/test-docker-action/notifications{?since,all,participating}",
+    "open_issues": 15,
+    "open_issues_count": 15,
+    "organization": "docker",
+    "owner": {
+      "avatar_url": "https://avatars.githubusercontent.com/u/5429470?v=4",
+      "email": "github@docker.com",
+      "events_url": "https://api.github.com/users/docker/events{/privacy}",
+      "followers_url": "https://api.github.com/users/docker/followers",
+      "following_url": "https://api.github.com/users/docker/following{/other_user}",
+      "gists_url": "https://api.github.com/users/docker/gists{/gist_id}",
+      "gravatar_id": "",
+      "html_url": "https://github.com/docker",
+      "id": 5429470,
+      "login": "docker",
+      "name": "docker",
+      "node_id": "MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+      "organizations_url": "https://api.github.com/users/docker/orgs",
+      "received_events_url": "https://api.github.com/users/docker/received_events",
+      "repos_url": "https://api.github.com/users/docker/repos",
+      "site_admin": false,
+      "starred_url": "https://api.github.com/users/docker/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/docker/subscriptions",
+      "type": "Organization",
+      "url": "https://api.github.com/users/docker",
+      "user_view_type": "public"
+    },
+    "private": true,
+    "pulls_url": "https://api.github.com/repos/docker/test-docker-action/pulls{/number}",
+    "pushed_at": 1761295593,
+    "releases_url": "https://api.github.com/repos/docker/test-docker-action/releases{/id}",
+    "size": 1374,
+    "ssh_url": "git@github.com:docker/test-docker-action.git",
+    "stargazers": 1,
+    "stargazers_count": 1,
+    "stargazers_url": "https://api.github.com/repos/docker/test-docker-action/stargazers",
+    "statuses_url": "https://api.github.com/repos/docker/test-docker-action/statuses/{sha}",
+    "subscribers_url": "https://api.github.com/repos/docker/test-docker-action/subscribers",
+    "subscription_url": "https://api.github.com/repos/docker/test-docker-action/subscription",
+    "svn_url": "https://github.com/docker/test-docker-action",
+    "tags_url": "https://api.github.com/repos/docker/test-docker-action/tags",
+    "teams_url": "https://api.github.com/repos/docker/test-docker-action/teams",
+    "topics": [],
+    "trees_url": "https://api.github.com/repos/docker/test-docker-action/git/trees{/sha}",
+    "updated_at": "2025-09-29T14:04:27Z",
+    "url": "https://api.github.com/repos/docker/test-docker-action",
+    "visibility": "internal",
+    "watchers": 1,
+    "watchers_count": 1,
+    "web_commit_signoff_required": false
+  },
+  "sender": {
+    "avatar_url": "https://avatars.githubusercontent.com/u/1951866?v=4",
+    "events_url": "https://api.github.com/users/crazy-max/events{/privacy}",
+    "followers_url": "https://api.github.com/users/crazy-max/followers",
+    "following_url": "https://api.github.com/users/crazy-max/following{/other_user}",
+    "gists_url": "https://api.github.com/users/crazy-max/gists{/gist_id}",
+    "gravatar_id": "",
+    "html_url": "https://github.com/crazy-max",
+    "id": 1951866,
+    "login": "crazy-max",
+    "node_id": "MDQ6VXNlcjE5NTE4NjY=",
+    "organizations_url": "https://api.github.com/users/crazy-max/orgs",
+    "received_events_url": "https://api.github.com/users/crazy-max/received_events",
+    "repos_url": "https://api.github.com/users/crazy-max/repos",
+    "site_admin": false,
+    "starred_url": "https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/crazy-max/subscriptions",
+    "type": "User",
+    "url": "https://api.github.com/users/crazy-max",
+    "user_view_type": "public"
+  }
+}
\ No newline at end of file
diff --git a/util/ghutil/fixtures/ghactx_push_expected.json b/util/ghutil/fixtures/ghactx_push_expected.json
new file mode 100644
index 00000000..c3e141db
--- /dev/null
+++ b/util/ghutil/fixtures/ghactx_push_expected.json
@@ -0,0 +1,263 @@
+{
+  "github_actor":"crazy-max",
+  "github_actor_id":"1951866",
+  "github_event_name":"push",
+  "github_event_payload":{
+    "after":"42f57536c4a55a4723772a03ead9da2940abb3d6",
+    "base_ref":null,
+    "before":"231cbfa88dca156930d8e7991b6cf57bc09fd090",
+    "commits":[
+      {
+        "author":{
+          "email":"1951866+crazy-max@users.noreply.github.com",
+          "name":"CrazyMax",
+          "username":"crazy-max"
+        },
+        "committer":{
+          "email":"1951866+crazy-max@users.noreply.github.com",
+          "name":"CrazyMax",
+          "username":"crazy-max"
+        },
+        "distinct":false,
+        "id":"3be79a9f0a68f4a7b93c001d377d9397663f6a5f",
+        "message":"ghutil",
+        "timestamp":"2025-10-24T10:27:10+02:00",
+        "tree_id":"134cdd78c695edc2dacc06fa212cbe393b84b93b",
+        "url":"https://github.com/docker/test-docker-action/commit/3be79a9f0a68f4a7b93c001d377d9397663f6a5f"
+      },
+      {
+        "author":{
+          "email":"1951866+crazy-max@users.noreply.github.com",
+          "name":"CrazyMax",
+          "username":"crazy-max"
+        },
+        "committer":{
+          "email":"noreply@github.com",
+          "name":"GitHub",
+          "username":"web-flow"
+        },
+        "distinct":true,
+        "id":"42f57536c4a55a4723772a03ead9da2940abb3d6",
+        "message":"Merge pull request #55 from crazy-max/ghutil\n\nghutil",
+        "timestamp":"2025-10-24T10:46:33+02:00",
+        "tree_id":"134cdd78c695edc2dacc06fa212cbe393b84b93b",
+        "url":"https://github.com/docker/test-docker-action/commit/42f57536c4a55a4723772a03ead9da2940abb3d6"
+      }
+    ],
+    "compare":"https://github.com/docker/test-docker-action/compare/231cbfa88dca...42f57536c4a5",
+    "created":false,
+    "deleted":false,
+    "enterprise":{
+      "avatar_url":"https://avatars.githubusercontent.com/b/19176?v=4",
+      "created_at":"2022-12-30T23:53:17Z",
+      "description":null,
+      "html_url":"https://github.com/enterprises/docker",
+      "id":19176,
+      "name":"Docker",
+      "node_id":"E_kgDNSug",
+      "slug":"docker",
+      "updated_at":"2025-10-20T20:39:05Z",
+      "website_url":null
+    },
+    "forced":false,
+    "head_commit":{
+      "author":{
+        "email":"1951866+crazy-max@users.noreply.github.com",
+        "name":"CrazyMax",
+        "username":"crazy-max"
+      },
+      "committer":{
+        "email":"noreply@github.com",
+        "name":"GitHub",
+        "username":"web-flow"
+      },
+      "distinct":true,
+      "id":"42f57536c4a55a4723772a03ead9da2940abb3d6",
+      "message":"Merge pull request #55 from crazy-max/ghutil\n\nghutil",
+      "timestamp":"2025-10-24T10:46:33+02:00",
+      "tree_id":"134cdd78c695edc2dacc06fa212cbe393b84b93b",
+      "url":"https://github.com/docker/test-docker-action/commit/42f57536c4a55a4723772a03ead9da2940abb3d6"
+    },
+    "organization":{
+      "avatar_url":"https://avatars.githubusercontent.com/u/5429470?v=4",
+      "description":"Docker helps developers bring their ideas to life by conquering the complexity of app development.",
+      "events_url":"https://api.github.com/orgs/docker/events",
+      "hooks_url":"https://api.github.com/orgs/docker/hooks",
+      "id":5429470,
+      "issues_url":"https://api.github.com/orgs/docker/issues",
+      "login":"docker",
+      "members_url":"https://api.github.com/orgs/docker/members{/member}",
+      "node_id":"MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+      "public_members_url":"https://api.github.com/orgs/docker/public_members{/member}",
+      "repos_url":"https://api.github.com/orgs/docker/repos",
+      "url":"https://api.github.com/orgs/docker"
+    },
+    "pusher":{
+      "email":"1951866+crazy-max@users.noreply.github.com",
+      "name":"crazy-max"
+    },
+    "ref":"refs/heads/master",
+    "repository":{
+      "allow_forking":true,
+      "archive_url":"https://api.github.com/repos/docker/test-docker-action/{archive_format}{/ref}",
+      "archived":false,
+      "assignees_url":"https://api.github.com/repos/docker/test-docker-action/assignees{/user}",
+      "blobs_url":"https://api.github.com/repos/docker/test-docker-action/git/blobs{/sha}",
+      "branches_url":"https://api.github.com/repos/docker/test-docker-action/branches{/branch}",
+      "clone_url":"https://github.com/docker/test-docker-action.git",
+      "collaborators_url":"https://api.github.com/repos/docker/test-docker-action/collaborators{/collaborator}",
+      "comments_url":"https://api.github.com/repos/docker/test-docker-action/comments{/number}",
+      "commits_url":"https://api.github.com/repos/docker/test-docker-action/commits{/sha}",
+      "compare_url":"https://api.github.com/repos/docker/test-docker-action/compare/{base}...{head}",
+      "contents_url":"https://api.github.com/repos/docker/test-docker-action/contents/{+path}",
+      "contributors_url":"https://api.github.com/repos/docker/test-docker-action/contributors",
+      "created_at":1596792180,
+      "custom_properties":{
+
+      },
+      "default_branch":"master",
+      "deployments_url":"https://api.github.com/repos/docker/test-docker-action/deployments",
+      "description":"Test \"Docker\" Actions",
+      "disabled":false,
+      "downloads_url":"https://api.github.com/repos/docker/test-docker-action/downloads",
+      "events_url":"https://api.github.com/repos/docker/test-docker-action/events",
+      "fork":false,
+      "forks":1,
+      "forks_count":1,
+      "forks_url":"https://api.github.com/repos/docker/test-docker-action/forks",
+      "full_name":"docker/test-docker-action",
+      "git_commits_url":"https://api.github.com/repos/docker/test-docker-action/git/commits{/sha}",
+      "git_refs_url":"https://api.github.com/repos/docker/test-docker-action/git/refs{/sha}",
+      "git_tags_url":"https://api.github.com/repos/docker/test-docker-action/git/tags{/sha}",
+      "git_url":"git://github.com/docker/test-docker-action.git",
+      "has_discussions":false,
+      "has_downloads":true,
+      "has_issues":true,
+      "has_pages":false,
+      "has_projects":false,
+      "has_wiki":false,
+      "homepage":"",
+      "hooks_url":"https://api.github.com/repos/docker/test-docker-action/hooks",
+      "html_url":"https://github.com/docker/test-docker-action",
+      "id":285789493,
+      "is_template":false,
+      "issue_comment_url":"https://api.github.com/repos/docker/test-docker-action/issues/comments{/number}",
+      "issue_events_url":"https://api.github.com/repos/docker/test-docker-action/issues/events{/number}",
+      "issues_url":"https://api.github.com/repos/docker/test-docker-action/issues{/number}",
+      "keys_url":"https://api.github.com/repos/docker/test-docker-action/keys{/key_id}",
+      "labels_url":"https://api.github.com/repos/docker/test-docker-action/labels{/name}",
+      "language":"JavaScript",
+      "languages_url":"https://api.github.com/repos/docker/test-docker-action/languages",
+      "license":{
+        "key":"mit",
+        "name":"MIT License",
+        "node_id":"MDc6TGljZW5zZTEz",
+        "spdx_id":"MIT",
+        "url":"https://api.github.com/licenses/mit"
+      },
+      "master_branch":"master",
+      "merges_url":"https://api.github.com/repos/docker/test-docker-action/merges",
+      "milestones_url":"https://api.github.com/repos/docker/test-docker-action/milestones{/number}",
+      "mirror_url":null,
+      "name":"test-docker-action",
+      "node_id":"MDEwOlJlcG9zaXRvcnkyODU3ODk0OTM=",
+      "notifications_url":"https://api.github.com/repos/docker/test-docker-action/notifications{?since,all,participating}",
+      "open_issues":15,
+      "open_issues_count":15,
+      "organization":"docker",
+      "owner":{
+        "avatar_url":"https://avatars.githubusercontent.com/u/5429470?v=4",
+        "email":"github@docker.com",
+        "events_url":"https://api.github.com/users/docker/events{/privacy}",
+        "followers_url":"https://api.github.com/users/docker/followers",
+        "following_url":"https://api.github.com/users/docker/following{/other_user}",
+        "gists_url":"https://api.github.com/users/docker/gists{/gist_id}",
+        "gravatar_id":"",
+        "html_url":"https://github.com/docker",
+        "id":5429470,
+        "login":"docker",
+        "name":"docker",
+        "node_id":"MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+        "organizations_url":"https://api.github.com/users/docker/orgs",
+        "received_events_url":"https://api.github.com/users/docker/received_events",
+        "repos_url":"https://api.github.com/users/docker/repos",
+        "site_admin":false,
+        "starred_url":"https://api.github.com/users/docker/starred{/owner}{/repo}",
+        "subscriptions_url":"https://api.github.com/users/docker/subscriptions",
+        "type":"Organization",
+        "url":"https://api.github.com/users/docker",
+        "user_view_type":"public"
+      },
+      "private":true,
+      "pulls_url":"https://api.github.com/repos/docker/test-docker-action/pulls{/number}",
+      "pushed_at":1761295593,
+      "releases_url":"https://api.github.com/repos/docker/test-docker-action/releases{/id}",
+      "size":1374,
+      "ssh_url":"git@github.com:docker/test-docker-action.git",
+      "stargazers":1,
+      "stargazers_count":1,
+      "stargazers_url":"https://api.github.com/repos/docker/test-docker-action/stargazers",
+      "statuses_url":"https://api.github.com/repos/docker/test-docker-action/statuses/{sha}",
+      "subscribers_url":"https://api.github.com/repos/docker/test-docker-action/subscribers",
+      "subscription_url":"https://api.github.com/repos/docker/test-docker-action/subscription",
+      "svn_url":"https://github.com/docker/test-docker-action",
+      "tags_url":"https://api.github.com/repos/docker/test-docker-action/tags",
+      "teams_url":"https://api.github.com/repos/docker/test-docker-action/teams",
+      "topics":[
+
+      ],
+      "trees_url":"https://api.github.com/repos/docker/test-docker-action/git/trees{/sha}",
+      "updated_at":"2025-09-29T14:04:27Z",
+      "url":"https://api.github.com/repos/docker/test-docker-action",
+      "visibility":"internal",
+      "watchers":1,
+      "watchers_count":1,
+      "web_commit_signoff_required":false
+    },
+    "sender":{
+      "avatar_url":"https://avatars.githubusercontent.com/u/1951866?v=4",
+      "events_url":"https://api.github.com/users/crazy-max/events{/privacy}",
+      "followers_url":"https://api.github.com/users/crazy-max/followers",
+      "following_url":"https://api.github.com/users/crazy-max/following{/other_user}",
+      "gists_url":"https://api.github.com/users/crazy-max/gists{/gist_id}",
+      "gravatar_id":"",
+      "html_url":"https://github.com/crazy-max",
+      "id":1951866,
+      "login":"crazy-max",
+      "node_id":"MDQ6VXNlcjE5NTE4NjY=",
+      "organizations_url":"https://api.github.com/users/crazy-max/orgs",
+      "received_events_url":"https://api.github.com/users/crazy-max/received_events",
+      "repos_url":"https://api.github.com/users/crazy-max/repos",
+      "site_admin":false,
+      "starred_url":"https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
+      "subscriptions_url":"https://api.github.com/users/crazy-max/subscriptions",
+      "type":"User",
+      "url":"https://api.github.com/users/crazy-max",
+      "user_view_type":"public"
+    }
+  },
+  "github_job":"ghutil",
+  "github_ref":"refs/heads/master",
+  "github_ref_name":"master",
+  "github_ref_protected":"false",
+  "github_ref_type":"branch",
+  "github_repository":"docker/test-docker-action",
+  "github_repository_id":"285789493",
+  "github_repository_owner":"docker",
+  "github_repository_owner_id":"5429470",
+  "github_run_attempt":"1",
+  "github_run_id":"18774637729",
+  "github_run_number":"3",
+  "github_runner_arch":"X64",
+  "github_runner_environment":"github-hosted",
+  "github_runner_image_os":"ubuntu24",
+  "github_runner_image_version":"20250929.60.1",
+  "github_runner_name":"GitHub Actions 1002237537",
+  "github_runner_os":"Linux",
+  "github_runner_tracking_id":"github_a790683c-a332-45df-b720-24d8d022bfc7",
+  "github_server_url":"https://github.com",
+  "github_triggering_actor":"crazy-max",
+  "github_workflow":"ghutil",
+  "github_workflow_ref":"docker/test-docker-action/.github/workflows/ghutil.yml@refs/heads/master",
+  "github_workflow_sha":"42f57536c4a55a4723772a03ead9da2940abb3d6"
+}
\ No newline at end of file
diff --git a/util/ghutil/fixtures/ghactx_tag.env b/util/ghutil/fixtures/ghactx_tag.env
new file mode 100644
index 00000000..12c4ff59
--- /dev/null
+++ b/util/ghutil/fixtures/ghactx_tag.env
@@ -0,0 +1,106 @@
+ACCEPT_EULA=Y
+ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE=/opt/actionarchivecache
+AGENT_TOOLSDIRECTORY=/opt/hostedtoolcache
+ANDROID_HOME=/usr/local/lib/android/sdk
+ANDROID_NDK=/usr/local/lib/android/sdk/ndk/27.3.13750724
+ANDROID_NDK_HOME=/usr/local/lib/android/sdk/ndk/27.3.13750724
+ANDROID_NDK_LATEST_HOME=/usr/local/lib/android/sdk/ndk/28.2.13676358
+ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/27.3.13750724
+ANDROID_SDK_ROOT=/usr/local/lib/android/sdk
+ANT_HOME=/usr/share/ant
+AZURE_EXTENSION_DIR=/opt/az/azcliextensions
+BOOTSTRAP_HASKELL_NONINTERACTIVE=1
+CHROMEWEBDRIVER=/usr/local/share/chromedriver-linux64
+CHROME_BIN=/usr/bin/google-chrome
+CI=true
+CONDA=/usr/share/miniconda
+DEBIAN_FRONTEND=noninteractive
+DOTNET_MULTILEVEL_LOOKUP=0
+DOTNET_NOLOGO=1
+DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
+EDGEWEBDRIVER=/usr/local/share/edge_driver
+ENABLE_RUNNER_TRACING=true
+GECKOWEBDRIVER=/usr/local/share/gecko_driver
+GHCUP_INSTALL_BASE_PREFIX=/usr/local
+GITHUB_ACTION=__run
+GITHUB_ACTIONS=true
+GITHUB_ACTION_REF=
+GITHUB_ACTION_REPOSITORY=
+GITHUB_ACTOR=crazy-max
+GITHUB_ACTOR_ID=1951866
+GITHUB_API_URL=https://api.github.com
+GITHUB_BASE_REF=
+GITHUB_ENV=/home/runner/work/_temp/_runner_file_commands/set_env_de9781d2-4b3b-4757-b530-2064deb19c41
+GITHUB_EVENT_NAME=push
+GITHUB_EVENT_PATH=./fixtures/ghactx_tag_event.json
+GITHUB_GRAPHQL_URL=https://api.github.com/graphql
+GITHUB_HEAD_REF=
+GITHUB_JOB=ghutil
+GITHUB_OUTPUT=/home/runner/work/_temp/_runner_file_commands/set_output_de9781d2-4b3b-4757-b530-2064deb19c41
+GITHUB_PATH=/home/runner/work/_temp/_runner_file_commands/add_path_de9781d2-4b3b-4757-b530-2064deb19c41
+GITHUB_REF=refs/tags/v2.8.0
+GITHUB_REF_NAME=v2.8.0
+GITHUB_REF_PROTECTED=false
+GITHUB_REF_TYPE=tag
+GITHUB_REPOSITORY=docker/test-docker-action
+GITHUB_REPOSITORY_ID=285789493
+GITHUB_REPOSITORY_OWNER=docker
+GITHUB_REPOSITORY_OWNER_ID=5429470
+GITHUB_RETENTION_DAYS=90
+GITHUB_RUN_ATTEMPT=1
+GITHUB_RUN_ID=18774710377
+GITHUB_RUN_NUMBER=4
+GITHUB_SERVER_URL=https://github.com
+GITHUB_SHA=42f57536c4a55a4723772a03ead9da2940abb3d6
+GITHUB_STATE=/home/runner/work/_temp/_runner_file_commands/save_state_de9781d2-4b3b-4757-b530-2064deb19c41
+GITHUB_STEP_SUMMARY=/home/runner/work/_temp/_runner_file_commands/step_summary_de9781d2-4b3b-4757-b530-2064deb19c41
+GITHUB_TRIGGERING_ACTOR=crazy-max
+GITHUB_WORKFLOW=ghutil
+GITHUB_WORKFLOW_REF=docker/test-docker-action/.github/workflows/ghutil.yml@refs/tags/v2.8.0
+GITHUB_WORKFLOW_SHA=42f57536c4a55a4723772a03ead9da2940abb3d6
+GITHUB_WORKSPACE=/home/runner/work/test-docker-action/test-docker-action
+GOROOT_1_22_X64=/opt/hostedtoolcache/go/1.22.12/x64
+GOROOT_1_23_X64=/opt/hostedtoolcache/go/1.23.12/x64
+GOROOT_1_24_X64=/opt/hostedtoolcache/go/1.24.7/x64
+GRADLE_HOME=/usr/share/gradle-9.1.0
+HOME=/home/runner
+HOMEBREW_CLEANUP_PERIODIC_FULL_DAYS=3650
+HOMEBREW_NO_AUTO_UPDATE=1
+INVOCATION_ID=cdb06b30bfa146fab2970772a489c540
+ImageOS=ubuntu24
+ImageVersion=20250929.60.1
+JAVA_HOME=/usr/lib/jvm/temurin-17-jdk-amd64
+JAVA_HOME_11_X64=/usr/lib/jvm/temurin-11-jdk-amd64
+JAVA_HOME_17_X64=/usr/lib/jvm/temurin-17-jdk-amd64
+JAVA_HOME_21_X64=/usr/lib/jvm/temurin-21-jdk-amd64
+JAVA_HOME_25_X64=/usr/lib/jvm/temurin-25-jdk-amd64
+JAVA_HOME_8_X64=/usr/lib/jvm/temurin-8-jdk-amd64
+JOURNAL_STREAM=9:12793
+LANG=C.UTF-8
+LOGNAME=runner
+MEMORY_PRESSURE_WATCH=/sys/fs/cgroup/system.slice/hosted-compute-agent.service/memory.pressure
+MEMORY_PRESSURE_WRITE=c29tZSAyMDAwMDAgMjAwMDAwMAA=
+NVM_DIR=/home/runner/.nvm
+PATH=/snap/bin:/home/runner/.local/bin:/opt/pipx_bin:/home/runner/.cargo/bin:/home/runner/.config/composer/vendor/bin:/usr/local/.ghcup/bin:/home/runner/.dotnet/tools:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
+PIPX_BIN_DIR=/opt/pipx_bin
+PIPX_HOME=/opt/pipx
+POWERSHELL_DISTRIBUTION_CHANNEL=GitHub-Actions-ubuntu24
+RUNNER_ARCH=X64
+RUNNER_ENVIRONMENT=github-hosted
+RUNNER_NAME=GitHub Actions 1002237589
+RUNNER_OS=Linux
+RUNNER_TEMP=/home/runner/work/_temp
+RUNNER_TOOL_CACHE=/opt/hostedtoolcache
+RUNNER_TRACKING_ID=github_dffa1c6c-c3b7-42d8-873e-a83b52f27716
+RUNNER_WORKSPACE=/home/runner/work/test-docker-action
+SELENIUM_JAR_PATH=/usr/share/java/selenium-server.jar
+SGX_AESM_ADDR=1
+SHELL=/bin/bash
+SHLVL=1
+SWIFT_PATH=/usr/share/swift/usr/bin
+SYSTEMD_EXEC_PID=1746
+USER=runner
+VCPKG_INSTALLATION_ROOT=/usr/local/share/vcpkg
+XDG_CONFIG_HOME=/home/runner/.config
+XDG_RUNTIME_DIR=/run/user/1001
+_=/usr/bin/env
\ No newline at end of file
diff --git a/util/ghutil/fixtures/ghactx_tag_event.json b/util/ghutil/fixtures/ghactx_tag_event.json
new file mode 100644
index 00000000..d8b93d09
--- /dev/null
+++ b/util/ghutil/fixtures/ghactx_tag_event.json
@@ -0,0 +1,193 @@
+{
+  "after": "42f57536c4a55a4723772a03ead9da2940abb3d6",
+  "base_ref": "refs/heads/master",
+  "before": "0000000000000000000000000000000000000000",
+  "commits": [],
+  "compare": "https://github.com/docker/test-docker-action/compare/v2.8.0",
+  "created": true,
+  "deleted": false,
+  "enterprise": {
+    "avatar_url": "https://avatars.githubusercontent.com/b/19176?v=4",
+    "created_at": "2022-12-30T23:53:17Z",
+    "description": null,
+    "html_url": "https://github.com/enterprises/docker",
+    "id": 19176,
+    "name": "Docker",
+    "node_id": "E_kgDNSug",
+    "slug": "docker",
+    "updated_at": "2025-10-20T20:39:05Z",
+    "website_url": null
+  },
+  "forced": false,
+  "head_commit": {
+    "author": {
+      "email": "1951866+crazy-max@users.noreply.github.com",
+      "name": "CrazyMax",
+      "username": "crazy-max"
+    },
+    "committer": {
+      "email": "noreply@github.com",
+      "name": "GitHub",
+      "username": "web-flow"
+    },
+    "distinct": true,
+    "id": "42f57536c4a55a4723772a03ead9da2940abb3d6",
+    "message": "Merge pull request #55 from crazy-max/ghutil\n\nghutil",
+    "timestamp": "2025-10-24T10:46:33+02:00",
+    "tree_id": "134cdd78c695edc2dacc06fa212cbe393b84b93b",
+    "url": "https://github.com/docker/test-docker-action/commit/42f57536c4a55a4723772a03ead9da2940abb3d6"
+  },
+  "organization": {
+    "avatar_url": "https://avatars.githubusercontent.com/u/5429470?v=4",
+    "description": "Docker helps developers bring their ideas to life by conquering the complexity of app development.",
+    "events_url": "https://api.github.com/orgs/docker/events",
+    "hooks_url": "https://api.github.com/orgs/docker/hooks",
+    "id": 5429470,
+    "issues_url": "https://api.github.com/orgs/docker/issues",
+    "login": "docker",
+    "members_url": "https://api.github.com/orgs/docker/members{/member}",
+    "node_id": "MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+    "public_members_url": "https://api.github.com/orgs/docker/public_members{/member}",
+    "repos_url": "https://api.github.com/orgs/docker/repos",
+    "url": "https://api.github.com/orgs/docker"
+  },
+  "pusher": {
+    "email": "1951866+crazy-max@users.noreply.github.com",
+    "name": "crazy-max"
+  },
+  "ref": "refs/tags/v2.8.0",
+  "repository": {
+    "allow_forking": true,
+    "archive_url": "https://api.github.com/repos/docker/test-docker-action/{archive_format}{/ref}",
+    "archived": false,
+    "assignees_url": "https://api.github.com/repos/docker/test-docker-action/assignees{/user}",
+    "blobs_url": "https://api.github.com/repos/docker/test-docker-action/git/blobs{/sha}",
+    "branches_url": "https://api.github.com/repos/docker/test-docker-action/branches{/branch}",
+    "clone_url": "https://github.com/docker/test-docker-action.git",
+    "collaborators_url": "https://api.github.com/repos/docker/test-docker-action/collaborators{/collaborator}",
+    "comments_url": "https://api.github.com/repos/docker/test-docker-action/comments{/number}",
+    "commits_url": "https://api.github.com/repos/docker/test-docker-action/commits{/sha}",
+    "compare_url": "https://api.github.com/repos/docker/test-docker-action/compare/{base}...{head}",
+    "contents_url": "https://api.github.com/repos/docker/test-docker-action/contents/{+path}",
+    "contributors_url": "https://api.github.com/repos/docker/test-docker-action/contributors",
+    "created_at": 1596792180,
+    "custom_properties": {},
+    "default_branch": "master",
+    "deployments_url": "https://api.github.com/repos/docker/test-docker-action/deployments",
+    "description": "Test \"Docker\" Actions",
+    "disabled": false,
+    "downloads_url": "https://api.github.com/repos/docker/test-docker-action/downloads",
+    "events_url": "https://api.github.com/repos/docker/test-docker-action/events",
+    "fork": false,
+    "forks": 1,
+    "forks_count": 1,
+    "forks_url": "https://api.github.com/repos/docker/test-docker-action/forks",
+    "full_name": "docker/test-docker-action",
+    "git_commits_url": "https://api.github.com/repos/docker/test-docker-action/git/commits{/sha}",
+    "git_refs_url": "https://api.github.com/repos/docker/test-docker-action/git/refs{/sha}",
+    "git_tags_url": "https://api.github.com/repos/docker/test-docker-action/git/tags{/sha}",
+    "git_url": "git://github.com/docker/test-docker-action.git",
+    "has_discussions": false,
+    "has_downloads": true,
+    "has_issues": true,
+    "has_pages": false,
+    "has_projects": false,
+    "has_wiki": false,
+    "homepage": "",
+    "hooks_url": "https://api.github.com/repos/docker/test-docker-action/hooks",
+    "html_url": "https://github.com/docker/test-docker-action",
+    "id": 285789493,
+    "is_template": false,
+    "issue_comment_url": "https://api.github.com/repos/docker/test-docker-action/issues/comments{/number}",
+    "issue_events_url": "https://api.github.com/repos/docker/test-docker-action/issues/events{/number}",
+    "issues_url": "https://api.github.com/repos/docker/test-docker-action/issues{/number}",
+    "keys_url": "https://api.github.com/repos/docker/test-docker-action/keys{/key_id}",
+    "labels_url": "https://api.github.com/repos/docker/test-docker-action/labels{/name}",
+    "language": "JavaScript",
+    "languages_url": "https://api.github.com/repos/docker/test-docker-action/languages",
+    "license": {
+      "key": "mit",
+      "name": "MIT License",
+      "node_id": "MDc6TGljZW5zZTEz",
+      "spdx_id": "MIT",
+      "url": "https://api.github.com/licenses/mit"
+    },
+    "master_branch": "master",
+    "merges_url": "https://api.github.com/repos/docker/test-docker-action/merges",
+    "milestones_url": "https://api.github.com/repos/docker/test-docker-action/milestones{/number}",
+    "mirror_url": null,
+    "name": "test-docker-action",
+    "node_id": "MDEwOlJlcG9zaXRvcnkyODU3ODk0OTM=",
+    "notifications_url": "https://api.github.com/repos/docker/test-docker-action/notifications{?since,all,participating}",
+    "open_issues": 15,
+    "open_issues_count": 15,
+    "organization": "docker",
+    "owner": {
+      "avatar_url": "https://avatars.githubusercontent.com/u/5429470?v=4",
+      "email": "github@docker.com",
+      "events_url": "https://api.github.com/users/docker/events{/privacy}",
+      "followers_url": "https://api.github.com/users/docker/followers",
+      "following_url": "https://api.github.com/users/docker/following{/other_user}",
+      "gists_url": "https://api.github.com/users/docker/gists{/gist_id}",
+      "gravatar_id": "",
+      "html_url": "https://github.com/docker",
+      "id": 5429470,
+      "login": "docker",
+      "name": "docker",
+      "node_id": "MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+      "organizations_url": "https://api.github.com/users/docker/orgs",
+      "received_events_url": "https://api.github.com/users/docker/received_events",
+      "repos_url": "https://api.github.com/users/docker/repos",
+      "site_admin": false,
+      "starred_url": "https://api.github.com/users/docker/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/docker/subscriptions",
+      "type": "Organization",
+      "url": "https://api.github.com/users/docker",
+      "user_view_type": "public"
+    },
+    "private": true,
+    "pulls_url": "https://api.github.com/repos/docker/test-docker-action/pulls{/number}",
+    "pushed_at": 1761295781,
+    "releases_url": "https://api.github.com/repos/docker/test-docker-action/releases{/id}",
+    "size": 1374,
+    "ssh_url": "git@github.com:docker/test-docker-action.git",
+    "stargazers": 1,
+    "stargazers_count": 1,
+    "stargazers_url": "https://api.github.com/repos/docker/test-docker-action/stargazers",
+    "statuses_url": "https://api.github.com/repos/docker/test-docker-action/statuses/{sha}",
+    "subscribers_url": "https://api.github.com/repos/docker/test-docker-action/subscribers",
+    "subscription_url": "https://api.github.com/repos/docker/test-docker-action/subscription",
+    "svn_url": "https://github.com/docker/test-docker-action",
+    "tags_url": "https://api.github.com/repos/docker/test-docker-action/tags",
+    "teams_url": "https://api.github.com/repos/docker/test-docker-action/teams",
+    "topics": [],
+    "trees_url": "https://api.github.com/repos/docker/test-docker-action/git/trees{/sha}",
+    "updated_at": "2025-10-24T08:46:37Z",
+    "url": "https://api.github.com/repos/docker/test-docker-action",
+    "visibility": "internal",
+    "watchers": 1,
+    "watchers_count": 1,
+    "web_commit_signoff_required": false
+  },
+  "sender": {
+    "avatar_url": "https://avatars.githubusercontent.com/u/1951866?v=4",
+    "events_url": "https://api.github.com/users/crazy-max/events{/privacy}",
+    "followers_url": "https://api.github.com/users/crazy-max/followers",
+    "following_url": "https://api.github.com/users/crazy-max/following{/other_user}",
+    "gists_url": "https://api.github.com/users/crazy-max/gists{/gist_id}",
+    "gravatar_id": "",
+    "html_url": "https://github.com/crazy-max",
+    "id": 1951866,
+    "login": "crazy-max",
+    "node_id": "MDQ6VXNlcjE5NTE4NjY=",
+    "organizations_url": "https://api.github.com/users/crazy-max/orgs",
+    "received_events_url": "https://api.github.com/users/crazy-max/received_events",
+    "repos_url": "https://api.github.com/users/crazy-max/repos",
+    "site_admin": false,
+    "starred_url": "https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/crazy-max/subscriptions",
+    "type": "User",
+    "url": "https://api.github.com/users/crazy-max",
+    "user_view_type": "public"
+  }
+}
\ No newline at end of file
diff --git a/util/ghutil/fixtures/ghactx_tag_expected.json b/util/ghutil/fixtures/ghactx_tag_expected.json
new file mode 100644
index 00000000..5174d318
--- /dev/null
+++ b/util/ghutil/fixtures/ghactx_tag_expected.json
@@ -0,0 +1,228 @@
+{
+  "github_actor":"crazy-max",
+  "github_actor_id":"1951866",
+  "github_event_name":"push",
+  "github_event_payload":{
+    "after":"42f57536c4a55a4723772a03ead9da2940abb3d6",
+    "base_ref":"refs/heads/master",
+    "before":"0000000000000000000000000000000000000000",
+    "commits":[
+
+    ],
+    "compare":"https://github.com/docker/test-docker-action/compare/v2.8.0",
+    "created":true,
+    "deleted":false,
+    "enterprise":{
+      "avatar_url":"https://avatars.githubusercontent.com/b/19176?v=4",
+      "created_at":"2022-12-30T23:53:17Z",
+      "description":null,
+      "html_url":"https://github.com/enterprises/docker",
+      "id":19176,
+      "name":"Docker",
+      "node_id":"E_kgDNSug",
+      "slug":"docker",
+      "updated_at":"2025-10-20T20:39:05Z",
+      "website_url":null
+    },
+    "forced":false,
+    "head_commit":{
+      "author":{
+        "email":"1951866+crazy-max@users.noreply.github.com",
+        "name":"CrazyMax",
+        "username":"crazy-max"
+      },
+      "committer":{
+        "email":"noreply@github.com",
+        "name":"GitHub",
+        "username":"web-flow"
+      },
+      "distinct":true,
+      "id":"42f57536c4a55a4723772a03ead9da2940abb3d6",
+      "message":"Merge pull request #55 from crazy-max/ghutil\n\nghutil",
+      "timestamp":"2025-10-24T10:46:33+02:00",
+      "tree_id":"134cdd78c695edc2dacc06fa212cbe393b84b93b",
+      "url":"https://github.com/docker/test-docker-action/commit/42f57536c4a55a4723772a03ead9da2940abb3d6"
+    },
+    "organization":{
+      "avatar_url":"https://avatars.githubusercontent.com/u/5429470?v=4",
+      "description":"Docker helps developers bring their ideas to life by conquering the complexity of app development.",
+      "events_url":"https://api.github.com/orgs/docker/events",
+      "hooks_url":"https://api.github.com/orgs/docker/hooks",
+      "id":5429470,
+      "issues_url":"https://api.github.com/orgs/docker/issues",
+      "login":"docker",
+      "members_url":"https://api.github.com/orgs/docker/members{/member}",
+      "node_id":"MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+      "public_members_url":"https://api.github.com/orgs/docker/public_members{/member}",
+      "repos_url":"https://api.github.com/orgs/docker/repos",
+      "url":"https://api.github.com/orgs/docker"
+    },
+    "pusher":{
+      "email":"1951866+crazy-max@users.noreply.github.com",
+      "name":"crazy-max"
+    },
+    "ref":"refs/tags/v2.8.0",
+    "repository":{
+      "allow_forking":true,
+      "archive_url":"https://api.github.com/repos/docker/test-docker-action/{archive_format}{/ref}",
+      "archived":false,
+      "assignees_url":"https://api.github.com/repos/docker/test-docker-action/assignees{/user}",
+      "blobs_url":"https://api.github.com/repos/docker/test-docker-action/git/blobs{/sha}",
+      "branches_url":"https://api.github.com/repos/docker/test-docker-action/branches{/branch}",
+      "clone_url":"https://github.com/docker/test-docker-action.git",
+      "collaborators_url":"https://api.github.com/repos/docker/test-docker-action/collaborators{/collaborator}",
+      "comments_url":"https://api.github.com/repos/docker/test-docker-action/comments{/number}",
+      "commits_url":"https://api.github.com/repos/docker/test-docker-action/commits{/sha}",
+      "compare_url":"https://api.github.com/repos/docker/test-docker-action/compare/{base}...{head}",
+      "contents_url":"https://api.github.com/repos/docker/test-docker-action/contents/{+path}",
+      "contributors_url":"https://api.github.com/repos/docker/test-docker-action/contributors",
+      "created_at":1596792180,
+      "custom_properties":{
+
+      },
+      "default_branch":"master",
+      "deployments_url":"https://api.github.com/repos/docker/test-docker-action/deployments",
+      "description":"Test \"Docker\" Actions",
+      "disabled":false,
+      "downloads_url":"https://api.github.com/repos/docker/test-docker-action/downloads",
+      "events_url":"https://api.github.com/repos/docker/test-docker-action/events",
+      "fork":false,
+      "forks":1,
+      "forks_count":1,
+      "forks_url":"https://api.github.com/repos/docker/test-docker-action/forks",
+      "full_name":"docker/test-docker-action",
+      "git_commits_url":"https://api.github.com/repos/docker/test-docker-action/git/commits{/sha}",
+      "git_refs_url":"https://api.github.com/repos/docker/test-docker-action/git/refs{/sha}",
+      "git_tags_url":"https://api.github.com/repos/docker/test-docker-action/git/tags{/sha}",
+      "git_url":"git://github.com/docker/test-docker-action.git",
+      "has_discussions":false,
+      "has_downloads":true,
+      "has_issues":true,
+      "has_pages":false,
+      "has_projects":false,
+      "has_wiki":false,
+      "homepage":"",
+      "hooks_url":"https://api.github.com/repos/docker/test-docker-action/hooks",
+      "html_url":"https://github.com/docker/test-docker-action",
+      "id":285789493,
+      "is_template":false,
+      "issue_comment_url":"https://api.github.com/repos/docker/test-docker-action/issues/comments{/number}",
+      "issue_events_url":"https://api.github.com/repos/docker/test-docker-action/issues/events{/number}",
+      "issues_url":"https://api.github.com/repos/docker/test-docker-action/issues{/number}",
+      "keys_url":"https://api.github.com/repos/docker/test-docker-action/keys{/key_id}",
+      "labels_url":"https://api.github.com/repos/docker/test-docker-action/labels{/name}",
+      "language":"JavaScript",
+      "languages_url":"https://api.github.com/repos/docker/test-docker-action/languages",
+      "license":{
+        "key":"mit",
+        "name":"MIT License",
+        "node_id":"MDc6TGljZW5zZTEz",
+        "spdx_id":"MIT",
+        "url":"https://api.github.com/licenses/mit"
+      },
+      "master_branch":"master",
+      "merges_url":"https://api.github.com/repos/docker/test-docker-action/merges",
+      "milestones_url":"https://api.github.com/repos/docker/test-docker-action/milestones{/number}",
+      "mirror_url":null,
+      "name":"test-docker-action",
+      "node_id":"MDEwOlJlcG9zaXRvcnkyODU3ODk0OTM=",
+      "notifications_url":"https://api.github.com/repos/docker/test-docker-action/notifications{?since,all,participating}",
+      "open_issues":15,
+      "open_issues_count":15,
+      "organization":"docker",
+      "owner":{
+        "avatar_url":"https://avatars.githubusercontent.com/u/5429470?v=4",
+        "email":"github@docker.com",
+        "events_url":"https://api.github.com/users/docker/events{/privacy}",
+        "followers_url":"https://api.github.com/users/docker/followers",
+        "following_url":"https://api.github.com/users/docker/following{/other_user}",
+        "gists_url":"https://api.github.com/users/docker/gists{/gist_id}",
+        "gravatar_id":"",
+        "html_url":"https://github.com/docker",
+        "id":5429470,
+        "login":"docker",
+        "name":"docker",
+        "node_id":"MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
+        "organizations_url":"https://api.github.com/users/docker/orgs",
+        "received_events_url":"https://api.github.com/users/docker/received_events",
+        "repos_url":"https://api.github.com/users/docker/repos",
+        "site_admin":false,
+        "starred_url":"https://api.github.com/users/docker/starred{/owner}{/repo}",
+        "subscriptions_url":"https://api.github.com/users/docker/subscriptions",
+        "type":"Organization",
+        "url":"https://api.github.com/users/docker",
+        "user_view_type":"public"
+      },
+      "private":true,
+      "pulls_url":"https://api.github.com/repos/docker/test-docker-action/pulls{/number}",
+      "pushed_at":1761295781,
+      "releases_url":"https://api.github.com/repos/docker/test-docker-action/releases{/id}",
+      "size":1374,
+      "ssh_url":"git@github.com:docker/test-docker-action.git",
+      "stargazers":1,
+      "stargazers_count":1,
+      "stargazers_url":"https://api.github.com/repos/docker/test-docker-action/stargazers",
+      "statuses_url":"https://api.github.com/repos/docker/test-docker-action/statuses/{sha}",
+      "subscribers_url":"https://api.github.com/repos/docker/test-docker-action/subscribers",
+      "subscription_url":"https://api.github.com/repos/docker/test-docker-action/subscription",
+      "svn_url":"https://github.com/docker/test-docker-action",
+      "tags_url":"https://api.github.com/repos/docker/test-docker-action/tags",
+      "teams_url":"https://api.github.com/repos/docker/test-docker-action/teams",
+      "topics":[
+
+      ],
+      "trees_url":"https://api.github.com/repos/docker/test-docker-action/git/trees{/sha}",
+      "updated_at":"2025-10-24T08:46:37Z",
+      "url":"https://api.github.com/repos/docker/test-docker-action",
+      "visibility":"internal",
+      "watchers":1,
+      "watchers_count":1,
+      "web_commit_signoff_required":false
+    },
+    "sender":{
+      "avatar_url":"https://avatars.githubusercontent.com/u/1951866?v=4",
+      "events_url":"https://api.github.com/users/crazy-max/events{/privacy}",
+      "followers_url":"https://api.github.com/users/crazy-max/followers",
+      "following_url":"https://api.github.com/users/crazy-max/following{/other_user}",
+      "gists_url":"https://api.github.com/users/crazy-max/gists{/gist_id}",
+      "gravatar_id":"",
+      "html_url":"https://github.com/crazy-max",
+      "id":1951866,
+      "login":"crazy-max",
+      "node_id":"MDQ6VXNlcjE5NTE4NjY=",
+      "organizations_url":"https://api.github.com/users/crazy-max/orgs",
+      "received_events_url":"https://api.github.com/users/crazy-max/received_events",
+      "repos_url":"https://api.github.com/users/crazy-max/repos",
+      "site_admin":false,
+      "starred_url":"https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
+      "subscriptions_url":"https://api.github.com/users/crazy-max/subscriptions",
+      "type":"User",
+      "url":"https://api.github.com/users/crazy-max",
+      "user_view_type":"public"
+    }
+  },
+  "github_job":"ghutil",
+  "github_ref":"refs/tags/v2.8.0",
+  "github_ref_name":"v2.8.0",
+  "github_ref_protected":"false",
+  "github_ref_type":"tag",
+  "github_repository":"docker/test-docker-action",
+  "github_repository_id":"285789493",
+  "github_repository_owner":"docker",
+  "github_repository_owner_id":"5429470",
+  "github_run_attempt":"1",
+  "github_run_id":"18774710377",
+  "github_run_number":"4",
+  "github_runner_arch":"X64",
+  "github_runner_environment":"github-hosted",
+  "github_runner_image_os":"ubuntu24",
+  "github_runner_image_version":"20250929.60.1",
+  "github_runner_name":"GitHub Actions 1002237589",
+  "github_runner_os":"Linux",
+  "github_runner_tracking_id":"github_dffa1c6c-c3b7-42d8-873e-a83b52f27716",
+  "github_server_url":"https://github.com",
+  "github_triggering_actor":"crazy-max",
+  "github_workflow":"ghutil",
+  "github_workflow_ref":"docker/test-docker-action/.github/workflows/ghutil.yml@refs/tags/v2.8.0",
+  "github_workflow_sha":"42f57536c4a55a4723772a03ead9da2940abb3d6"
+}
\ No newline at end of file
diff --git a/util/ghutil/ghutil.go b/util/ghutil/ghutil.go
new file mode 100644
index 00000000..e1ca13bf
--- /dev/null
+++ b/util/ghutil/ghutil.go
@@ -0,0 +1,145 @@
+package ghutil
+
+import (
+	"encoding/json"
+	"os"
+	"strconv"
+
+	"github.com/pkg/errors"
+)
+
+func GithubActionsContext() ([]byte, error) {
+	m := make(map[string]any)
+
+	// Detect if running in GitHub Actions
+	// "GITHUB_ACTIONS": "true"
+	if gha, ok := os.LookupEnv("GITHUB_ACTIONS"); !ok {
+		return nil, nil
+	} else if v, _ := strconv.ParseBool(gha); !v {
+		return nil, nil
+	}
+
+	// Skip if required event information is not available
+	githubEventName := os.Getenv("GITHUB_EVENT_NAME")
+	githubEventPath := os.Getenv("GITHUB_EVENT_PATH")
+	if githubEventName == "" || githubEventPath == "" {
+		return nil, nil
+	}
+
+	// GitHub event
+	// "GITHUB_EVENT_NAME": "push"
+	// "GITHUB_EVENT_PATH": "/home/runner/work/_temp/_github_workflow/event.json"
+	m["github_event_name"] = githubEventName
+	dt, err := os.ReadFile(githubEventPath)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to read GITHUB_EVENT_PATH %q", githubEventPath)
+	}
+	var evt map[string]any
+	if err := json.Unmarshal(dt, &evt); err != nil {
+		return nil, errors.Wrapf(err, "failed to unmarshal GitHub event payload from %q", githubEventPath)
+	}
+	m["github_event_payload"] = evt
+
+	// GitHub actor
+	// "GITHUB_ACTOR": "crazy-max"
+	// "GITHUB_ACTOR_ID": "1951866"
+	// "GITHUB_TRIGGERING_ACTOR": "crazy-max"
+	githubActor := os.Getenv("GITHUB_ACTOR")
+	githubActorID := os.Getenv("GITHUB_ACTOR_ID")
+	githubTriggeringActor := os.Getenv("GITHUB_TRIGGERING_ACTOR")
+	if githubActor != "" && githubActorID != "" && githubTriggeringActor != "" {
+		m["github_actor"] = githubActor
+		m["github_actor_id"] = githubActorID
+		m["github_triggering_actor"] = githubTriggeringActor
+	}
+
+	// GitHub ref
+	// "GITHUB_REF": "refs/heads/master"
+	// "GITHUB_REF_NAME": "master"
+	// "GITHUB_REF_PROTECTED": "true"
+	// "GITHUB_REF_TYPE": "branch"
+	githubRef := os.Getenv("GITHUB_REF")
+	githubRefName := os.Getenv("GITHUB_REF_NAME")
+	githubRefProtected := os.Getenv("GITHUB_REF_PROTECTED")
+	githubRefType := os.Getenv("GITHUB_REF_TYPE")
+	if githubRef != "" && githubRefName != "" && githubRefProtected != "" && githubRefType != "" {
+		m["github_ref"] = githubRef
+		m["github_ref_name"] = githubRefName
+		m["github_ref_protected"] = githubRefProtected
+		m["github_ref_type"] = githubRefType
+	}
+
+	// GitHub repository
+	// "GITHUB_REPOSITORY": "crazy-max/ghaction-dump-context"
+	// "GITHUB_REPOSITORY_ID": "286865579"
+	// "GITHUB_REPOSITORY_OWNER": "crazy-max"
+	// "GITHUB_REPOSITORY_OWNER_ID": "1951866"
+	githubRepository := os.Getenv("GITHUB_REPOSITORY")
+	githubRepositoryID := os.Getenv("GITHUB_REPOSITORY_ID")
+	githubRepositoryOwner := os.Getenv("GITHUB_REPOSITORY_OWNER")
+	githubRepositoryOwnerID := os.Getenv("GITHUB_REPOSITORY_OWNER_ID")
+	if githubRepository != "" && githubRepositoryID != "" && githubRepositoryOwner != "" && githubRepositoryOwnerID != "" {
+		m["github_repository"] = githubRepository
+		m["github_repository_id"] = githubRepositoryID
+		m["github_repository_owner"] = githubRepositoryOwner
+		m["github_repository_owner_id"] = githubRepositoryOwnerID
+	}
+
+	// GitHub workflow
+	// "GITHUB_JOB": "dump"
+	// "GITHUB_RUN_ATTEMPT": "1"
+	// "GITHUB_RUN_ID": "18715517508"
+	// "GITHUB_RUN_NUMBER": "617"
+	// "GITHUB_SERVER_URL": "https://github.com"
+	// "GITHUB_WORKFLOW": "ci"
+	// "GITHUB_WORKFLOW_REF": "crazy-max/ghaction-dump-context/.github/workflows/ci.yml@refs/heads/master"
+	// "GITHUB_WORKFLOW_SHA": "391d493f5fb04040f2a2770b33f86f3f62518392"
+	githubJob := os.Getenv("GITHUB_JOB")
+	githubRunAttempt := os.Getenv("GITHUB_RUN_ATTEMPT")
+	githubRunID := os.Getenv("GITHUB_RUN_ID")
+	githubRunNumber := os.Getenv("GITHUB_RUN_NUMBER")
+	githubServerURL := os.Getenv("GITHUB_SERVER_URL")
+	githubWorkflow := os.Getenv("GITHUB_WORKFLOW")
+	githubWorkflowRef := os.Getenv("GITHUB_WORKFLOW_REF")
+	githubWorkflowSHA := os.Getenv("GITHUB_WORKFLOW_SHA")
+	if githubJob != "" && githubRunAttempt != "" && githubRunID != "" && githubRunNumber != "" && githubServerURL != "" && githubWorkflow != "" && githubWorkflowRef != "" && githubWorkflowSHA != "" {
+		m["github_job"] = githubJob
+		m["github_run_attempt"] = githubRunAttempt
+		m["github_run_id"] = githubRunID
+		m["github_run_number"] = githubRunNumber
+		m["github_server_url"] = githubServerURL
+		m["github_workflow"] = githubWorkflow
+		m["github_workflow_ref"] = githubWorkflowRef
+		m["github_workflow_sha"] = githubWorkflowSHA
+	}
+
+	// GitHub runner information
+	// "ImageOS": "ubuntu24"
+	// "ImageVersion": "20250929.60.1"
+	// "RUNNER_ARCH": "X64"
+	// "RUNNER_ENVIRONMENT": "github-hosted"
+	// "RUNNER_NAME": "GitHub Actions 1000091440"
+	// "RUNNER_OS": "Linux"
+	// "RUNNER_TEMP": "/home/runner/work/_temp"
+	// "RUNNER_TOOL_CACHE": "/opt/hostedtoolcache"
+	// "RUNNER_TRACKING_ID": "github_7bd1d852-eaeb-423f-8fa4-4ddb797c652f"
+	// "RUNNER_WORKSPACE": "/home/runner/work/ghaction-dump-context"
+	githubRunnerImageOS := os.Getenv("ImageOS")
+	githubRunnerImageVersion := os.Getenv("ImageVersion")
+	githubRunnerOS := os.Getenv("RUNNER_OS")
+	githubRunnerArch := os.Getenv("RUNNER_ARCH")
+	githubRunnerEnvironment := os.Getenv("RUNNER_ENVIRONMENT")
+	githubRunnerTrackingID := os.Getenv("RUNNER_TRACKING_ID")
+	githubRunnerName := os.Getenv("RUNNER_NAME")
+	if githubRunnerImageOS != "" && githubRunnerImageVersion != "" && githubRunnerOS != "" && githubRunnerArch != "" && githubRunnerEnvironment != "" && githubRunnerTrackingID != "" && githubRunnerName != "" {
+		m["github_runner_image_os"] = githubRunnerImageOS
+		m["github_runner_image_version"] = githubRunnerImageVersion
+		m["github_runner_os"] = githubRunnerOS
+		m["github_runner_arch"] = githubRunnerArch
+		m["github_runner_environment"] = githubRunnerEnvironment
+		m["github_runner_tracking_id"] = githubRunnerTrackingID
+		m["github_runner_name"] = githubRunnerName
+	}
+
+	return json.Marshal(m)
+}



Test patch:
diff --git a/dap/adapter_test.go b/dap/adapter_test.go
index 1017849c..1e990ada 100644
--- a/dap/adapter_test.go
+++ b/dap/adapter_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/json"
 	"io"
+	"path/filepath"
 	"testing"
 	"time"
 
@@ -69,6 +70,70 @@ func TestLaunch(t *testing.T) {
 	eg.Wait()
 }
 
+// TestSetBreakpoints will test sending a setBreakpoints request with no breakpoints.
+// The response should be an empty array instead of null in the JSON.
+func TestSetBreakpoints(t *testing.T) {
+	adapter, conn, client := NewTestAdapter[common.Config](t)
+
+	ctx, cancel := context.WithTimeoutCause(context.Background(), 10*time.Second, context.DeadlineExceeded)
+	defer cancel()
+
+	eg, _ := errgroup.WithContext(ctx)
+	eg.Go(func() error {
+		_, err := adapter.Start(ctx, conn)
+		assert.NoError(t, err)
+		return nil
+	})
+
+	var (
+		initialized    = make(chan struct{})
+		setBreakpoints <-chan *dap.SetBreakpointsResponse
+	)
+
+	client.RegisterEvent("initialized", func(em dap.EventMessage) {
+		setBreakpoints = DoRequest[*dap.SetBreakpointsResponse](t, client, &dap.SetBreakpointsRequest{
+			Request: dap.Request{Command: "setBreakpoints"},
+			Arguments: dap.SetBreakpointsArguments{
+				Source:      dap.Source{Name: "Dockerfile", Path: filepath.Join(t.TempDir(), "Dockerfile")},
+				Breakpoints: []dap.SourceBreakpoint{},
+			},
+		})
+		close(initialized)
+	})
+
+	eg.Go(func() error {
+		initializeResp := <-DoRequest[*dap.InitializeResponse](t, client, &dap.InitializeRequest{
+			Request: dap.Request{Command: "initialize"},
+		})
+		assert.True(t, initializeResp.Success)
+		assert.True(t, initializeResp.Body.SupportsConfigurationDoneRequest)
+
+		launchResp := <-DoRequest[*dap.LaunchResponse](t, client, &dap.LaunchRequest{
+			Request: dap.Request{Command: "launch"},
+		})
+		assert.True(t, launchResp.Success)
+
+		// We should have received the initialized event.
+		select {
+		case <-initialized:
+		default:
+			assert.Fail(t, "did not receive initialized event")
+		}
+
+		select {
+		case setBreakpointsResp := <-setBreakpoints:
+			assert.True(t, setBreakpointsResp.Success)
+			assert.Len(t, setBreakpointsResp.Body.Breakpoints, 0)
+			assert.NotNil(t, setBreakpointsResp.Body.Breakpoints, "breakpoints should be an empty array instead of null in the JSON")
+		case <-time.After(10 * time.Second):
+			assert.Fail(t, "did not receive setBreakpoints response")
+		}
+		return nil
+	})
+
+	eg.Wait()
+}
+
 func NewTestAdapter[C LaunchConfig](t *testing.T) (*Adapter[C], Conn, *Client) {
 	t.Helper()
 
diff --git a/tests/create.go b/tests/create.go
index d49f34bb..09335197 100644
--- a/tests/create.go
+++ b/tests/create.go
@@ -3,6 +3,7 @@ package tests
 import (
 	"fmt"
 	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 
@@ -23,6 +24,7 @@ var createTests = []func(t *testing.T, sb integration.Sandbox){
 	testCreateMemoryLimit,
 	testCreateRestartAlways,
 	testCreateRemoteContainer,
+	testCreateWithProvenanceGHA,
 }
 
 func testCreateMemoryLimit(t *testing.T, sb integration.Sandbox) {
@@ -108,3 +110,31 @@ func testCreateRemoteContainer(t *testing.T, sb integration.Sandbox) {
 	}
 	require.Fail(t, "remote builder is not running")
 }
+
+func testCreateWithProvenanceGHA(t *testing.T, sb integration.Sandbox) {
+	if !isDockerContainerWorker(sb) {
+		t.Skip("only testing with docker-container worker")
+	}
+
+	var builderName string
+	t.Cleanup(func() {
+		if builderName == "" {
+			return
+		}
+		out, err := rmCmd(sb, withArgs(builderName))
+		require.NoError(t, err, out)
+	})
+
+	ghep := filepath.Join(t.TempDir(), "event.json")
+	require.NoError(t, os.WriteFile(ghep, []byte(`{"test":{"foo":"bar"}}`), 0644))
+
+	out, err := createCmd(sb,
+		withArgs("--driver", "docker-container"),
+		withEnv("GITHUB_ACTIONS=true", "GITHUB_EVENT_NAME=push", "GITHUB_EVENT_PATH="+ghep),
+	)
+	require.NoError(t, err, out)
+	builderName = strings.TrimSpace(out)
+
+	out, err = inspectCmd(sb, withArgs(builderName, "--bootstrap"))
+	require.NoError(t, err, out)
+}
diff --git a/util/ghutil/ghutil_test.go b/util/ghutil/ghutil_test.go
new file mode 100644
index 00000000..85005a89
--- /dev/null
+++ b/util/ghutil/ghutil_test.go
@@ -0,0 +1,28 @@
+package ghutil
+
+import (
+	"os"
+	"path"
+	"testing"
+
+	"github.com/compose-spec/compose-go/v2/dotenv"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGithubActionsContext(t *testing.T) {
+	for _, tt := range []string{"pr", "push", "tag"} {
+		t.Run(tt, func(t *testing.T) {
+			envs, err := dotenv.Read(path.Join("fixtures", "ghactx_"+tt+".env"))
+			require.NoError(t, err)
+			for k, v := range envs {
+				t.Setenv(k, v)
+			}
+			ghactx, err := GithubActionsContext()
+			require.NoError(t, err)
+			require.NotNil(t, ghactx)
+			expected, err := os.ReadFile("fixtures/ghactx_" + tt + "_expected.json")
+			require.NoError(t, err)
+			require.JSONEq(t, string(expected), string(ghactx))
+		})
+	}
+}