diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml
index 32a0088822d..7cb74fc5d89 100644
--- a/.github/workflows/job-test-in-container.yml
+++ b/.github/workflows/job-test-in-container.yml
@@ -35,6 +35,10 @@ on:
         required: false
         default: false
         type: boolean
+    outputs:
+      artifact:
+        description: "Artifact generated by this job"
+        value: ${{ jobs.test.outputs.artifact }}
 
 env:
   GOTOOLCHAIN: local
@@ -55,6 +59,8 @@ jobs:
     defaults:
       run:
         shell: bash
+    outputs:
+      artifact: ${{ steps.artifact-upload.outputs.artifact-url }}
 
     env:
       # https://github.com/containerd/nerdctl/issues/622
@@ -161,9 +167,9 @@ jobs:
             && args=(test-integration ./hack/test-integration.sh -test.allow-modify-users=true) \
             || args=(test-integration-${{ inputs.target }} /test-integration-rootless.sh ./hack/test-integration.sh)
           if [ "${{ inputs.ipv6 }}" == true ]; then
-            docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.only-ipv6 -test.target=${{ inputs.binary }}
+            docker run --name test-runner --network host -t --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.only-ipv6 -test.target=${{ inputs.binary }}
           else
-            docker run -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.target=${{ inputs.binary }}
+            docker run --name test-runner -t --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.target=${{ inputs.binary }}
           fi
       # FIXME: this NEEDS to go away
       - name: "Run: integration tests (flaky)"
@@ -175,7 +181,42 @@ jobs:
             && args=(test-integration ./hack/test-integration.sh) \
             || args=(test-integration-${{ inputs.target }} /test-integration-rootless.sh ./hack/test-integration.sh)
           if [ "${{ inputs.ipv6 }}" == true ]; then
-            docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.only-ipv6 -test.target=${{ inputs.binary }}
+            docker run --name test-runner-flaky --network host -t --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.only-ipv6 -test.target=${{ inputs.binary }}
           else
-            docker run -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.target=${{ inputs.binary }}
+            docker run --name test-runner-flaky -t --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.target=${{ inputs.binary }}
           fi
+
+      - name: "Wrap: collector"
+        if: ${{ failure() || success() }}
+        run: |
+          # Get the reports from inside the containers
+          [ "${{ inputs.target }}" == "rootful" ] && src=/root || src=/home/rootless
+          mkdir -p ~/report
+          docker cp test-runner:$src/nerdctl-test-report ~/report/main || true
+          # Flaky may not have run
+          docker cp test-runner-flaky:$src/nerdctl-test-report ~/report/flaky 2>/dev/null || true
+          # Add metadata info to the runs
+          . ./mod/wax/scripts/collector.sh
+          collect::metadata \
+            "runner=${{ inputs.runner }}" \
+            "binary=${{ inputs.binary }}" \
+            "canary=${{ inputs.canary }}" \
+            "target=${{ inputs.target }}" \
+            "ipv6=${{ inputs.ipv6 }}" \
+            "containerd-version=${{ inputs.containerd-version }}" \
+            "rootlesskit-version=${{ inputs.rootlesskit-version }}" \
+            "attempt=$GITHUB_RUN_ATTEMPT" \
+            "sha=$GITHUB_SHA" \
+            "id=$GITHUB_RUN_ID" \
+            "number=$GITHUB_RUN_NUMBER" \
+            > ~/report/main/metadata.json || true
+            [ ! -e ~/report/flaky ] || cp ~/report/main/metadata.json ~/report/flaky/metadata.json || true
+
+      - name: "Wrap: upload"
+        id: artifact-upload
+        if: ${{ failure() || success() }}
+        uses: actions/upload-artifact@v4
+        with:
+          path: ~/report/*
+          retention-days: 1
+          name: wax-${{ inputs.runner }}-${{ inputs.binary }}-${{ inputs.canary }}-${{ inputs.target }}-${{ inputs.ipv6 }}-${{ inputs.containerd-version }}-${{ inputs.rootlesskit-version }}
diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml
index f760c74780f..ab4b2a22d41 100644
--- a/.github/workflows/job-test-in-host.yml
+++ b/.github/workflows/job-test-in-host.yml
@@ -43,6 +43,10 @@ on:
       linux-cni-sha:
         required: true
         type: string
+    outputs:
+      artifact:
+        description: "Artifact generated by this job"
+        value: ${{ jobs.test.outputs.artifact }}
 
 env:
   GOTOOLCHAIN: local
@@ -60,6 +64,8 @@ jobs:
     defaults:
       run:
         shell: bash
+    outputs:
+      artifact: ${{ steps.artifact-upload.outputs.artifact-url }}
 
     env:
       SHOULD_RUN: "yes"
@@ -107,6 +113,7 @@ jobs:
           if [ "${{ contains(inputs.binary, 'docker') }}" == true ]; then
             echo "::group:: configure cdi for docker"
             sudo mkdir -p /etc/docker
+            sudo touch /etc/docker/daemon.json
             sudo jq '.features.cdi = true' /etc/docker/daemon.json | sudo tee /etc/docker/daemon.json.tmp && sudo mv /etc/docker/daemon.json.tmp /etc/docker/daemon.json
             echo "::endgroup::"
             echo "::group:: downgrade docker to the specific version we want to test (${{ inputs.docker-version }})"
@@ -183,14 +190,22 @@ jobs:
           make install-dev-tools
           echo "::endgroup::"
 
+
+      - if: ${{ env.SHOULD_RUN == 'yes' }}
+        name: "Init: prepare artifacts directories"
+        run: |
+          mkdir -p ~/report/main
+          mkdir -p ~/report/ipv6
+          mkdir -p ~/report/flaky
+
       # ipv6 is tested only on linux
-      - if: ${{ contains(inputs.runner, 'ubuntu') && env.SHOULD_RUN == 'yes' }}
+      - if: ${{ env.SHOULD_RUN == 'yes' && contains(inputs.runner, 'ubuntu' )}}
         name: "Run (linux): integration tests (IPv6)"
         run: |
           . ./hack/github/action-helpers.sh
           github::md::h2 "ipv6" >> "$GITHUB_STEP_SUMMARY"
 
-          ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-ipv6
+          WAX_REPORT_LOCATION=$HOME/report/ipv6 ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-ipv6
 
       - if: ${{ env.SHOULD_RUN == 'yes' }}
         name: "Run: integration tests"
@@ -198,7 +213,7 @@ jobs:
           . ./hack/github/action-helpers.sh
           github::md::h2 "non-flaky" >> "$GITHUB_STEP_SUMMARY"
 
-          ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-flaky=false
+          WAX_REPORT_LOCATION=$HOME/report/main ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-flaky=false
 
       # FIXME: this must go
       - if: ${{ env.SHOULD_RUN == 'yes' }}
@@ -207,4 +222,42 @@ jobs:
           . ./hack/github/action-helpers.sh
           github::md::h2 "flaky" >> "$GITHUB_STEP_SUMMARY"
 
-          ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-flaky=true
+          WAX_REPORT_LOCATION=$HOME/report/flaky ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-flaky=true
+
+      - name: "Wrap: collector"
+        if: ${{ env.SHOULD_RUN == 'yes' && (failure() || success()) }}
+        run: |
+          # Add metadata info to the runs
+          . ./mod/wax/scripts/collector.sh
+          collect::metadata \
+            "runner=${{ inputs.runner }}" \
+            "binary=${{ inputs.binary }}" \
+            "canary=${{ inputs.canary }}" \
+            "ipv6=false" \
+            "attempt=$GITHUB_RUN_ATTEMPT" \
+            "sha=$GITHUB_SHA" \
+            "id=$GITHUB_RUN_ID" \
+            "number=$GITHUB_RUN_NUMBER" \
+            > ~/report/main/metadata.json || true
+
+          collect::metadata \
+            "runner=${{ inputs.runner }}" \
+            "binary=${{ inputs.binary }}" \
+            "canary=${{ inputs.canary }}" \
+            "ipv6=true" \
+            "attempt=$GITHUB_RUN_ATTEMPT" \
+            "sha=$GITHUB_SHA" \
+            "id=$GITHUB_RUN_ID" \
+            "number=$GITHUB_RUN_NUMBER" \
+            > ~/report/ipv6/metadata.json || true
+
+          cp ~/report/main/metadata.json ~/report/flaky/metadata.json || true
+
+      - name: "Wrap: upload"
+        id: artifact-upload
+        if: ${{ env.SHOULD_RUN == 'yes' && (failure() || success()) }}
+        uses: actions/upload-artifact@v4
+        with:
+          path: ~/report/*
+          retention-days: 1
+          name: wax-${{ inputs.runner }}-${{ inputs.binary }}-${{ inputs.canary }}
diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml
index c6d6f6a4e7a..968a162aaa4 100644
--- a/.github/workflows/workflow-lint.yml
+++ b/.github/workflows/workflow-lint.yml
@@ -78,3 +78,13 @@ jobs:
       go-version: ${{ matrix.go-version }}
       runner: ubuntu-24.04
       canary: ${{ matrix.canary && true || false }}
+
+
+  reporter:
+    if: ${{ failure() || success() }}
+    name: "DEBUG"
+    runs-on: ubuntu-24.04
+    steps:
+      - name: Process
+        run: |
+          printf '::error title=TESTTHIS,file=.github/workflows/job-test-in-container.yml,line=1::ONE
TWO %0A; %0A; %0A; `THREE`\n'
diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml
index b11d4578ed7..84852c43587 100644
--- a/.github/workflows/workflow-test.yml
+++ b/.github/workflows/workflow-test.yml
@@ -147,3 +147,33 @@ jobs:
       containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8
       linux-cni-version: v1.7.1
       linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098
+
+  reporter:
+    if: ${{ failure() || success() }}
+    name: "reporter${{ inputs.hack }}"
+    needs:
+      - test-integration-host
+      - test-integration-container
+    runs-on: ubuntu-24.04
+    steps:
+      - name: Fetch Repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 2
+      - name: "Init: install go"
+        uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5  # v5.5.0
+        with:
+          go-version: 1.24
+          check-latest: true
+      - name: Download all workflow run artifacts
+        uses: actions/download-artifact@v4
+        with:
+          path: ~/report
+      - name: Process
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          cd ./mod/wax
+          go build -o ../../wax ./cmd/wax
+          cd -
+          ./wax ~/report
diff --git a/cmd/nerdctl/main_test.go b/cmd/nerdctl/main_test.go
index bcd84434556..f4139800139 100644
--- a/cmd/nerdctl/main_test.go
+++ b/cmd/nerdctl/main_test.go
@@ -33,6 +33,11 @@ func TestMain(m *testing.M) {
 	testutil.M(m)
 }
 
+func TestWax(t *testing.T) {
+	t.Log("This test is voluntarily failing")
+	t.FailNow()
+}
+
 // TestUnknownCommand tests https://github.com/containerd/nerdctl/issues/487
 func TestUnknownCommand(t *testing.T) {
 	testCase := nerdtest.Setup()
diff --git a/hack/github/action-helpers.sh b/hack/github/action-helpers.sh
index 62244ee6fbc..da25dcb69b8 100755
--- a/hack/github/action-helpers.sh
+++ b/hack/github/action-helpers.sh
@@ -117,3 +117,8 @@ github::timer::format() {
   [[ "$m" == 0 ]] || printf "%d minutes " "$m"
   printf '%d seconds' "$s"
 }
+
+#          echo "::error title=ErrorReport::MEH .github/workflows/job-test-in-host.yml${{steps.artifact-upload.outputs.artifact-url}}"
+#          echo "::notice title=NoticeReport::SHEESH ${{steps.artifact-upload.outputs.artifact-url}}"
+#          echo "::error file=cmd/nerdctl/main_test_test.go,line=1,endLine=10,title=AgainErrorReport::FOO ${{steps.artifact-upload.outputs.artifact-url}}"
+#          echo "::error file=cmd/nerdctl/main_test.go,line=38,endLine=41,title=AgainErrorReport::BLA ${{steps.artifact-upload.outputs.artifact-url}}"
diff --git a/hack/github/gotestsum-reporter.sh b/hack/github/gotestsum-reporter.sh
index 872fc25f03d..5a33a909014 100755
--- a/hack/github/gotestsum-reporter.sh
+++ b/hack/github/gotestsum-reporter.sh
@@ -14,6 +14,11 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+# DEPRECATED: favor post-processing inside integration.sh rather than using a post-run hook.
+# Reasons are:
+# - post-run hook do not work on windows
+# - post-run hook is limited to a single run of gotestsum, while it is desirable to process multiple runs together
+
 # shellcheck disable=SC2034,SC2015
 set -o errexit -o errtrace -o functrace -o nounset -o pipefail
 root="$(cd "$(dirname "${BASH_SOURCE[0]:-$PWD}")" 2>/dev/null 1>&2 && pwd)"
diff --git a/hack/test-integration.sh b/hack/test-integration.sh
index 3d1a21365a5..91df67d0e1d 100755
--- a/hack/test-integration.sh
+++ b/hack/test-integration.sh
@@ -19,38 +19,79 @@ set -o errexit -o errtrace -o functrace -o nounset -o pipefail
 root="$(cd "$(dirname "${BASH_SOURCE[0]:-$PWD}")" 2>/dev/null 1>&2 && pwd)"
 readonly root
 
-if [[ "$(id -u)" = "0" ]]; then
-  # Ensure securityfs is mounted for apparmor to work
-  if ! mountpoint -q /sys/kernel/security; then
-    mount -tsecurityfs securityfs /sys/kernel/security
-  fi
+# If no argument is provided, run both flaky and not-flaky test suites.
+if [ "$#" == 0 ]; then
+  "$root"/integration.sh -test.only-flaky=false
+  "$root"/integration.sh -test.only-flaky=true
+  exit
 fi
 
+##### Import helper libraries
+# shellcheck source=/dev/null
+. "$root"/../mod/wax/scripts/collector.sh
+
+##### Configuration
+# Where to store report files
+readonly report_location="${WAX_REPORT_LOCATION:-$HOME/nerdctl-test-report}"
+# Where to store gotestsum log file
+readonly gotestsum_log_main="$report_location"/test-integration.log
+readonly gotestsum_log_flaky="$report_location"/test-integration-flaky.log
+# Total run timeout
 readonly timeout="60m"
+# Number of retries for flaky tests
 readonly retries="2"
-readonly needsudo="${WITH_SUDO:-}"
-
-# See https://github.com/containerd/nerdctl/blob/main/docs/testing/README.md#about-parallelization
-args=(--format=testname --jsonfile /tmp/test-integration.log --packages="$root"/../cmd/nerdctl/...)
-# FIXME: not working on windows. Need to change approach: move away from --post-run-command and
-# just process the log file. This might also allow multi-steps/multi-target results aggregation.
-[ "$(uname -s)" != "Linux" ] || args+=(--post-run-command "$root"/github/gotestsum-reporter.sh)
-
-if [ "$#" == 0 ]; then
-  "$root"/test-integration.sh -test.only-flaky=false
-  "$root"/test-integration.sh -test.only-flaky=true
-  exit
-fi
+readonly need_sudo="${WITH_SUDO:-}"
 
+##### Prepare gotestsum arguments
+mkdir -p "$report_location"
+# Format and packages to test
+args=(--format=testname --packages="$root"/../cmd/nerdctl/...)
+# Log file
+gotestsum_log="$gotestsum_log_main"
 for arg in "$@"; do
   if [ "$arg" == "-test.only-flaky=true" ] || [ "$arg" == "-test.only-flaky" ]; then
     args+=("--rerun-fails=$retries")
+    gotestsum_log="$gotestsum_log_flaky"
     break
   fi
 done
+args+=(--jsonfile "$gotestsum_log" --)
+
+##### Append go test arguments
+# Honor sudo
+[ "$need_sudo" != true ] && [ "$need_sudo" != yes ] && [ "$need_sudo" != 1 ] || args+=(-exec sudo)
+# About `-p 1`, see https://github.com/containerd/nerdctl/blob/main/docs/testing/README.md#about-parallelization
+args+=(-timeout="$timeout" -p 1 -args -test.allow-kill-daemon "$@")
 
-if [ "$needsudo" == "true" ] || [ "$needsudo" == "yes" ] || [ "$needsudo" == "1" ]; then
-  gotestsum "${args[@]}" -- -timeout="$timeout" -p 1 -exec sudo -args -test.allow-kill-daemon "$@"
-else
-  gotestsum "${args[@]}" -- -timeout="$timeout" -p 1 -args -test.allow-kill-daemon "$@"
+# FIXME: this should not be the responsibility of the test script
+# Instead, it should be in the Dockerfile (or other stack provisioning script) - eg: /etc/systemd/system/securityfs.service
+# [Unit]
+# Description=Kernel Security File System
+# DefaultDependencies=no
+# Before=sysinit.target
+# Before=apparmor.service
+# ConditionSecurity=apparmor
+# ConditionPathIsMountPoint=!/sys/kernel/security
+#
+# [Service]
+# Type=oneshot
+# ExecStart=/bin/mount -t securityfs -o nosuid,nodev,noexec securityfs /sys/kernel/security
+#
+# [Install]
+# WantedBy=sysinit.target
+if [[ "$(id -u)" = "0" ]]; then
+  # Ensure securityfs is mounted for apparmor to work
+  if ! mountpoint -q /sys/kernel/security; then
+    mount -tsecurityfs securityfs /sys/kernel/security
+  fi
 fi
+
+##### Run it
+ex=0
+gotestsum "${args[@]}" || ex=$?
+
+##### Post: collect logs into the report location
+collect::logs "$report_location"
+
+# Honor gotestsum exit code
+exit "$ex"
\ No newline at end of file
diff --git a/mod/wax/Makefile b/mod/wax/Makefile
new file mode 100644
index 00000000000..6c21581b962
--- /dev/null
+++ b/mod/wax/Makefile
@@ -0,0 +1,209 @@
+#   Copyright The containerd Authors.
+
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+
+#       http://www.apache.org/licenses/LICENSE-2.0
+
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+# project-checks is broken.
+# See https://github.com/containerd/nerdctl/pull/3889
+
+##########################
+# Configuration
+##########################
+ORG_PREFIXES := "github.com/containerd"
+
+MAKEFILE_DIR := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
+VERSION ?= $(shell git -C $(MAKEFILE_DIR) describe --match 'v[0-9]*' --dirty='.m' --always --tags)
+VERSION_TRIMMED := $(VERSION:v%=%)
+REVISION ?= $(shell git -C $(MAKEFILE_DIR) rev-parse HEAD)$(shell if ! git -C $(MAKEFILE_DIR) diff --no-ext-diff --quiet --exit-code; then echo .m; fi)
+LINT_COMMIT_RANGE ?= main..HEAD
+
+##########################
+# Helpers
+##########################
+ifdef VERBOSE
+	VERBOSE_FLAG := -v
+	VERBOSE_FLAG_LONG := --verbose
+endif
+
+ifndef NO_COLOR
+    NC := \033[0m
+    GREEN := \033[1;32m
+    ORANGE := \033[1;33m
+endif
+
+# Helpers
+recursive_wildcard=$(wildcard $1$2) $(foreach e,$(wildcard $1*),$(call recursive_wildcard,$e/,$2))
+
+define title
+	@printf "$(GREEN)____________________________________________________________________________________________________\n"
+	@printf "$(GREEN)%*s\n" $$(( ( $(shell echo "🐯$(1) 🐯" | wc -c ) + 100 ) / 2 )) "🐯$(1) 🐯"
+	@printf "$(GREEN)____________________________________________________________________________________________________\n$(ORANGE)"
+endef
+
+define footer
+	@printf "$(GREEN)> %s: done!\n" "$(1)"
+	@printf "$(GREEN)____________________________________________________________________________________________________\n$(NC)"
+endef
+
+##########################
+# High-level tasks definitions
+##########################
+lint: lint-go-all lint-yaml lint-shell lint-commits lint-headers lint-mod lint-licenses-all
+test: test-unit test-unit-race test-unit-bench
+unit: test-unit test-unit-race test-unit-bench
+fix: fix-mod fix-go-all
+
+##########################
+# Linting tasks
+##########################
+lint-go:
+	$(call title, $@: $(GOOS))
+	@cd $(MAKEFILE_DIR) \
+		&& golangci-lint run $(VERBOSE_FLAG_LONG) ./...
+	$(call footer, $@)
+
+lint-go-all:
+	$(call title, $@)
+	@cd $(MAKEFILE_DIR) \
+		&& GOOS=darwin make lint-go \
+		&& GOOS=freebsd make lint-go \
+		&& GOOS=linux make lint-go \
+		&& GOOS=windows make lint-go
+	$(call footer, $@)
+
+lint-yaml:
+	$(call title, $@)
+	@cd $(MAKEFILE_DIR) \
+		&& yamllint .
+	$(call footer, $@)
+
+lint-shell: $(call recursive_wildcard,$(MAKEFILE_DIR)/,*.sh)
+	$(call title, $@)
+	@shellcheck -a -x $^
+	$(call footer, $@)
+
+lint-commits:
+	$(call title, $@)
+	@cd $(MAKEFILE_DIR) \
+		&& git-validation $(VERBOSE_FLAG) -run DCO,short-subject,dangling-whitespace -range "$(LINT_COMMIT_RANGE)"
+	$(call footer, $@)
+
+lint-headers:
+	$(call title, $@)
+	@cd $(MAKEFILE_DIR) \
+		&& ltag -t "./hack/headers" --check -v
+	$(call footer, $@)
+
+lint-mod:
+	$(call title, $@)
+	@cd $(MAKEFILE_DIR) \
+		&& go mod tidy --diff
+	$(call footer, $@)
+
+# FIXME: go-licenses cannot find LICENSE from root of repo when submodule is imported:
+# https://github.com/google/go-licenses/issues/186
+# This is impacting gotest.tools
+lint-licenses:
+	$(call title, $@: $(GOOS))
+	@cd $(MAKEFILE_DIR) \
+		&& go-licenses check --include_tests --allowed_licenses=Apache-2.0,BSD-2-Clause,BSD-3-Clause,MIT,MPL-2.0 \
+		  --ignore gotest.tools \
+		  ./...
+	$(call footer, $@)
+
+lint-licenses-all:
+	$(call title, $@)
+	@cd $(MAKEFILE_DIR) \
+		&& GOOS=darwin make lint-licenses \
+		&& GOOS=freebsd make lint-licenses \
+		&& GOOS=linux make lint-licenses \
+		&& GOOS=windows make lint-licenses
+	$(call footer, $@)
+
+##########################
+# Automated fixing tasks
+##########################
+fix-go:
+	$(call title, $@: $(GOOS))
+	@cd $(MAKEFILE_DIR) \
+		&& golangci-lint run --fix
+	$(call footer, $@)
+
+fix-go-all:
+	$(call title, $@)
+	@cd $(MAKEFILE_DIR) \
+		&& GOOS=darwin make fix-go \
+		&& GOOS=freebsd make fix-go \
+		&& GOOS=linux make fix-go \
+		&& GOOS=windows make fix-go
+	$(call footer, $@)
+
+fix-mod:
+	$(call title, $@)
+	@cd $(MAKEFILE_DIR) \
+		&& go mod tidy
+	$(call footer, $@)
+
+up:
+	$(call title, $@)
+	@cd $(MAKEFILE_DIR) \
+		&& go get -u ./...
+	$(call footer, $@)
+
+##########################
+# Development tools installation
+##########################
+install-dev-tools:
+	$(call title, $@)
+	# golangci: v2.0.2 (2024-03-26)
+	# git-validation: main (2025-02-25)
+	# ltag: main (2025-03-04)
+	# go-licenses: v2.0.0-alpha.1 (2024-06-27)
+	# stubbing go-licenses with dependency upgrade due to non-compatibility with golang 1.25rc1
+	# Issue: https://github.com/google/go-licenses/issues/312
+	@cd $(MAKEFILE_DIR) \
+		&& go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@2b224c2cf4c9f261c22a16af7f8ca6408467f338 \
+		&& go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \
+		&& go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \
+		&& go install github.com/Shubhranshu153/go-licenses/v2@f8c503d1357dffb6c97ed3b94e912ab294dde24a \
+		&& go install gotest.tools/gotestsum@0d9599e513d70e5792bb9334869f82f6e8b53d4d
+	@echo "Remember to add \$$HOME/go/bin to your path"
+	$(call footer, $@)
+
+##########################
+# Testing tasks
+##########################
+test-unit:
+	$(call title, $@)
+	@HIGHK_EXPERIMENTAL_FD=true go test $(VERBOSE_FLAG) $(MAKEFILE_DIR)/...
+	$(call footer, $@)
+
+test-unit-bench:
+	$(call title, $@)
+	@go test $(VERBOSE_FLAG) $(MAKEFILE_DIR)/... -bench=.
+	$(call footer, $@)
+
+test-unit-race:
+	$(call title, $@)
+	@HIGHK_EXPERIMENTAL_FD=true CGO_ENABLED=1 go test $(VERBOSE_FLAG) $(MAKEFILE_DIR)/... -race
+	$(call footer, $@)
+
+.PHONY: \
+	lint \
+	fix \
+	test \
+	up \
+	unit \
+	install-dev-tools \
+	lint-commits lint-go lint-go-all lint-headers lint-licenses lint-licenses-all lint-mod lint-shell lint-yaml \
+	fix-go fix-go-all fix-mod \
+	test-unit test-unit-race test-unit-bench
\ No newline at end of file
diff --git a/mod/wax/README.md b/mod/wax/README.md
new file mode 100644
index 00000000000..d9a4eb10d62
--- /dev/null
+++ b/mod/wax/README.md
@@ -0,0 +1,9 @@
+# Wax
+
+> polish your CI
+
+A tool to annotate pull requests and generate reports from gotestsum logs.
+
+## TL;DR
+
+TBD.
diff --git a/mod/wax/analyzer/analyzer.go b/mod/wax/analyzer/analyzer.go
new file mode 100644
index 00000000000..afa388fcb6c
--- /dev/null
+++ b/mod/wax/analyzer/analyzer.go
@@ -0,0 +1,52 @@
+package analyzer
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+func FindLocation(testName string, packageName string, base string) (string, int, int) {
+	root := packageName[len(base)+1:]
+	topTestName := strings.Split(testName, "/")[0]
+	foundFile := ""
+	foundLine := 0
+	foundEndLine := 0
+
+	err := filepath.WalkDir(root, func(path string, info os.DirEntry, err error) error {
+		if foundFile != "" {
+			return nil
+		}
+
+		if err != nil {
+			return err
+		}
+
+		l, _ := os.Stat(path)
+		if !l.IsDir() && strings.HasSuffix(filepath.Base(path), "_test.go") {
+			cc, _ := os.ReadFile(path)
+			for id, line := range strings.Split(string(cc), "\n") {
+				if line == fmt.Sprintf("func %s(t *testing.T) {", topTestName) {
+					foundFile = path
+					foundLine = id + 1
+				} else if foundFile != "" && line == "}" {
+					foundEndLine = id + 1
+					break
+				}
+			}
+		}
+
+		return nil
+	})
+
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	return foundFile, foundLine, foundEndLine
+}
+
+func LocationURL(commit, file string, start, end int, base string) string {
+	return fmt.Sprintf("https://%s/blob/%s/%s#L%d-L%d", base, commit, file, start, end)
+}
diff --git a/mod/wax/api/github.go b/mod/wax/api/github.go
new file mode 100644
index 00000000000..3bd8cb17cba
--- /dev/null
+++ b/mod/wax/api/github.go
@@ -0,0 +1,47 @@
+package api
+
+import (
+	"context"
+
+	"github.com/google/go-github/v73/github"
+)
+
+type Issue = github.Issue
+
+type status string
+
+const Open status = "open"
+const Closed status = "closed"
+const All status = "all"
+
+type Github struct {
+	client *github.Client
+}
+
+func New(token string) *Github {
+	return &Github{
+		client: github.NewClient(nil).WithAuthToken(token),
+	}
+}
+
+func (gh *Github) Issues(ctx context.Context, state status, owner, repo string) ([]*Issue, error) {
+	opts := &github.IssueListByRepoOptions{
+		ListOptions: github.ListOptions{PerPage: 100},
+		State:       string(state),
+	}
+
+	var allIssues []*Issue
+	for {
+		issues, response, err := gh.client.Issues.ListByRepo(ctx, owner, repo, opts)
+		if err != nil {
+			return nil, err
+		}
+		allIssues = append(allIssues, issues...)
+		if response.NextPage == 0 {
+			break
+		}
+		opts.ListOptions.Page = response.NextPage
+	}
+
+	return allIssues, nil
+}
diff --git a/mod/wax/cmd/wax/main.go b/mod/wax/cmd/wax/main.go
new file mode 100644
index 00000000000..54764dee2b2
--- /dev/null
+++ b/mod/wax/cmd/wax/main.go
@@ -0,0 +1,87 @@
+package main
+
+import (
+	"context"
+	"github.com/containerd/nerdctl/mod/wax/format/action"
+	"os"
+	"os/exec"
+	"strings"
+
+	log "github.com/sirupsen/logrus"
+
+	"github.com/containerd/nerdctl/mod/wax/api"
+	"github.com/containerd/nerdctl/mod/wax/gotestsum"
+	"github.com/containerd/nerdctl/mod/wax/report"
+)
+
+func main() {
+	//mainLine := "::error endLine=84,file=cmd/nerdctl/ipfs/ipfs_compose_linux_test.go,line=41,title=Test failed in a flaky way::%0A```%0A=== RUN   TestIPFSCompNoBuild%0A=== PAUSE TestIPFSCompNoBuild%0A=== CONT\n"
+	////rgx := "^::([^:]+) file=([^,:]+),line=(\\d+)(?:,endLine=(\\d+))?,title=([^,:]+)::WAX-MARK-(.*)-WAX-MARK$"
+
+	//rgx := "(?m)^::([^:]+) (?:endLine=(\\d+),)?file=([^,:]+),line=(\\d+),title=([^,:]+)::(.*)"
+	//r := regexp.MustCompile(rgx)
+	//res := r.FindStringSubmatch(mainLine)
+	//fmt.Println("LLALA")
+	//fmt.Println(">>>", res)
+	//return
+
+	tk := os.Getenv("GITHUB_TOKEN")
+	sha := os.Getenv("GITHUB_SHA")
+	rep := strings.Split(os.Getenv("GITHUB_REPOSITORY"), "/")
+	gh := api.New(tk)
+	issues, err := gh.Issues(context.Background(), api.Open, rep[0], rep[1])
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	reportsRoot := os.Args[1]
+
+	run, _ := gotestsum.FromLogs(gotestsum.FindLogs(reportsRoot, "test-integration")...)
+	ls, _ := exec.Command("git", "diff", "--name-only", "-r", "HEAD^1", "HEAD").CombinedOutput()
+	changedFiles := strings.Split(string(ls), "\n")
+	topfile := changedFiles[0]
+
+	alwaysFailingTests := run.FailedAlways()
+	for _, failedTest := range alwaysFailingTests {
+		var found *api.Issue
+		// Look for the top-level test, not subtests
+		compare := strings.Split(string(failedTest.Test), "/")[0]
+		for _, issue := range issues {
+			if strings.Contains(*issue.Title, compare) {
+				found = issue
+				break
+			}
+		}
+		if found != nil {
+			failedTest.KnownIssue = *found.HTMLURL
+		}
+	}
+
+	onceFailingTests := run.FailedAtLeastOnceButNotAlways()
+	for _, failedTest := range onceFailingTests {
+		var found *api.Issue
+		// Look for the top-level test, not subtests
+		compare := strings.Split(string(failedTest.Test), "/")[0]
+		for _, issue := range issues {
+			if strings.Contains(*issue.Title, compare) {
+				found = issue
+				break
+			}
+		}
+		if found != nil {
+			failedTest.KnownIssue = *found.HTMLURL
+		}
+	}
+
+	testsThatNeverRan := run.NeverRan()
+
+	// ::error file=.github/matchers/wax.json,line=1,endLine=1,title=Tests that have NOT run in any pipeline::WAX-MARK-
+
+	//"regexp": "^([^:]+):(\\d+):(\\d+):\\s+(error|warning):\\s+(.*)$",
+	//
+	//action.AddMatcher(os.Stdout, "wax", `^(\w+):\s+(\d+)\s+(\d+)\s+(.+)$`, 1, 2, 3, 4, 5)
+
+	action.AddMatcher(os.Stdout, "wax", "^::([^:]+) (?:endLine=(\\\\d+),)?file=([^,:]+),line=(\\\\d+),title=([^,:]+)::(.*)$", 1, 3, 4, 2, 5, 6)
+	report.Annotate("github.com/"+strings.Join(rep, "/"), "v2", sha, alwaysFailingTests, onceFailingTests, testsThatNeverRan, topfile)
+	action.RemoveMatcher(os.Stdout, "wax")
+}
diff --git a/mod/wax/format/action/action.go b/mod/wax/format/action/action.go
new file mode 100644
index 00000000000..f07819ca4d1
--- /dev/null
+++ b/mod/wax/format/action/action.go
@@ -0,0 +1,29 @@
+package action
+
+import (
+	"fmt"
+	"io"
+	"sort"
+	"strings"
+)
+
+func command(out io.Writer, command string, params map[string]string, message string) {
+	par := []string{}
+	for k, v := range params {
+		par = append(par, fmt.Sprintf("%s=%s", k, v))
+	}
+	sort.Strings(par)
+	_, _ = fmt.Fprintf(out, "::%s %s::%s\n", command, strings.Join(par, ","), strings.ReplaceAll(message, "\n", "
")) // "%0A"))
+}
+
+func GroupStart(out io.Writer, title string) {
+	command(out, "group", nil, title)
+}
+
+func GroupEnd(out io.Writer) {
+	command(out, "endgroup", nil, "")
+}
+
+func Debug(out io.Writer, message string) {
+	command(out, "group", nil, message)
+}
diff --git a/mod/wax/format/action/annotation.go b/mod/wax/format/action/annotation.go
new file mode 100644
index 00000000000..9702832efc8
--- /dev/null
+++ b/mod/wax/format/action/annotation.go
@@ -0,0 +1,60 @@
+package action
+
+import (
+	"io"
+	"strconv"
+)
+
+const (
+	warningLevel = "warning"
+	noticeLevel  = "notice"
+	errorLevel   = "error"
+	fileKey      = "file"
+	columnKey    = "col"
+	endColumnKey = "endColumn"
+	lineKey      = "line"
+	endLineKey   = "endLine"
+	titleKey     = "title"
+)
+
+func annotation(out io.Writer, level, file string, col, endColumn, line, endLine int, title, message string) {
+	params := map[string]string{}
+
+	if file != "" {
+		params[fileKey] = file
+	}
+
+	if col != 0 {
+		params[columnKey] = strconv.Itoa(col)
+	}
+
+	if line != 0 {
+		params[lineKey] = strconv.Itoa(line)
+	}
+
+	if endColumn != 0 {
+		params[endColumnKey] = strconv.Itoa(endColumn)
+	}
+
+	if endLine != 0 {
+		params[endLineKey] = strconv.Itoa(endLine)
+	}
+
+	if title != "" {
+		params[titleKey] = title
+	}
+
+	command(out, level, params, message)
+}
+
+func Notice(out io.Writer, file string, line, endLine, col, endColumn int, title, message string) {
+	annotation(out, noticeLevel, file, col, endColumn, line, endLine, title, message)
+}
+
+func Warning(out io.Writer, file string, line, endLine, col, endColumn int, title, message string) {
+	annotation(out, warningLevel, file, col, endColumn, line, endLine, title, message)
+}
+
+func Error(out io.Writer, file string, line, endLine, col, endColumn int, title, message string) {
+	annotation(out, errorLevel, file, col, endColumn, line, endLine, title, message)
+}
diff --git a/mod/wax/format/action/matcher.go b/mod/wax/format/action/matcher.go
new file mode 100644
index 00000000000..b8ec4d245df
--- /dev/null
+++ b/mod/wax/format/action/matcher.go
@@ -0,0 +1,66 @@
+package action
+
+import (
+	"encoding/json"
+	"io"
+	"os"
+	"path/filepath"
+)
+
+const (
+	addMatcherCommand    = "add-matcher"
+	removeMatcherCommand = "remove-matcher"
+	permission           = 0o600
+)
+
+type matcher struct {
+	ProblemMatcher *ProblemMatcher `json:"problemMatcher"`
+}
+
+type ProblemMatcherPattern struct {
+	Regexp    string `json:"regexp"`
+	Severity  int    `json:"severity"`
+	File      int    `json:"file"`
+	Line      int    `json:"line"`
+	EndLine   int    `json:"endLine"`
+	Column    int    `json:"column"`
+	EndColumn int    `json:"endColumn"`
+	Message   int    `json:"message"`
+	Title     int    `json:"title"`
+}
+
+type ProblemMatcher []struct {
+	Owner   string                   `json:"owner"`
+	Pattern []*ProblemMatcherPattern `json:"pattern"`
+}
+
+func AddMatcher(out io.Writer, owner, regexp string, severity, endLine, file, line, title, message int) {
+	mtc := &matcher{
+		ProblemMatcher: &ProblemMatcher{
+			{
+				Owner: owner,
+				Pattern: []*ProblemMatcherPattern{
+					{
+						Regexp:   regexp,
+						Severity: severity,
+						File:     file,
+						Line:     line,
+						EndLine:  endLine,
+						Title:    title,
+						Message:  message,
+					},
+				},
+			},
+		},
+	}
+	matcherLocation := filepath.Join(os.TempDir(), owner+".json")
+	m, _ := json.MarshalIndent(mtc, "", "  ")
+	_ = os.WriteFile(matcherLocation, m, permission)
+	command(out, addMatcherCommand, nil, matcherLocation)
+}
+
+func RemoveMatcher(out io.Writer, owner string) {
+	command(out, removeMatcherCommand, map[string]string{
+		"owner": owner,
+	}, "")
+}
diff --git a/mod/wax/format/markdown/markdown.go b/mod/wax/format/markdown/markdown.go
new file mode 100644
index 00000000000..ed2cb056005
--- /dev/null
+++ b/mod/wax/format/markdown/markdown.go
@@ -0,0 +1,45 @@
+package markdown
+
+import (
+	"fmt"
+	"io"
+	"strings"
+)
+
+func markdown(out io.Writer, prefix, message, suffix string) {
+	_, _ = fmt.Fprintf(out, "%s %s %s\n", prefix, message, suffix)
+}
+
+func H1(out io.Writer, message string) {
+	markdown(out, "#", message, "")
+}
+
+func H2(out io.Writer, message string) {
+	markdown(out, "##", message, "")
+}
+
+func H3(out io.Writer, message string) {
+	markdown(out, "###", message, "")
+}
+
+func Blockquote(out io.Writer, message string) {
+	markdown(out, ">", message, "")
+}
+
+func Table(out io.Writer, headers []string, rows [][]string) {
+	markdown(out, "|", strings.Join(headers, "|"), "|")
+	markdown(out, "|", strings.Repeat(" ----- |", len(headers)), "")
+	for _, row := range rows {
+		markdown(out, "|", strings.Join(row, "|"), "|")
+	}
+}
+
+func Pie(out io.Writer, title string, values map[string]string) {
+	markdown(out, "```", "mermaid", "")
+	markdown(out, "", "pie", "")
+	markdown(out, "title", title, "")
+	for label, value := range values {
+		markdown(out, fmt.Sprintf("%q", label), value, "")
+	}
+	markdown(out, "", "", "```")
+}
diff --git a/mod/wax/go.mod b/mod/wax/go.mod
new file mode 100644
index 00000000000..806b7ada404
--- /dev/null
+++ b/mod/wax/go.mod
@@ -0,0 +1,21 @@
+module github.com/containerd/nerdctl/mod/wax
+
+go 1.23.5
+
+require (
+	github.com/google/go-github/v73 v73.0.0
+	github.com/sirupsen/logrus v1.9.3
+	gotest.tools/gotestsum v1.12.3
+)
+
+require (
+	github.com/bitfield/gotestdox v0.2.2 // indirect
+	github.com/fatih/color v1.18.0 // indirect
+	github.com/google/go-querystring v1.1.0 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	golang.org/x/sync v0.15.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/term v0.32.0 // indirect
+	golang.org/x/text v0.17.0 // indirect
+)
diff --git a/mod/wax/go.sum b/mod/wax/go.sum
new file mode 100644
index 00000000000..a8093a3b8e8
--- /dev/null
+++ b/mod/wax/go.sum
@@ -0,0 +1,49 @@
+github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CDsnE=
+github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/go-github/v73 v73.0.0 h1:aR+Utnh+Y4mMkS+2qLQwcQ/cF9mOTpdwnzlaw//rG24=
+github.com/google/go-github/v73 v73.0.0/go.mod h1:fa6w8+/V+edSU0muqdhCVY7Beh1M8F1IlQPZIANKIYw=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
+golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
+golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
+golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
+golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/gotestsum v1.12.3 h1:jFwenGJ0RnPkuKh2VzAYl1mDOJgbhobBDeL2W1iEycs=
+gotest.tools/gotestsum v1.12.3/go.mod h1:Y1+e0Iig4xIRtdmYbEV7K7H6spnjc1fX4BOuUhWw2Wk=
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
diff --git a/mod/wax/gotestsum/helpers.go b/mod/wax/gotestsum/helpers.go
new file mode 100644
index 00000000000..ecdb2e5a3e0
--- /dev/null
+++ b/mod/wax/gotestsum/helpers.go
@@ -0,0 +1,113 @@
+package gotestsum
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+func testIntersection(slice1, slice2 []*FailedTest) []*FailedTest {
+	seen := make(map[string]bool)
+	result := []*FailedTest{}
+
+	for _, val1 := range slice1 {
+		tn1 := val1.Package + "/" + string(val1.Test)
+		for _, val2 := range slice2 {
+			tn2 := val2.Package + "/" + string(val2.Test)
+			if tn1 == tn2 {
+				if _, ok := seen[tn1]; !ok {
+					seen[tn1] = true
+					result = append(result, val1)
+					break
+				}
+			}
+		}
+	}
+
+	return result
+}
+
+func testTestIntersection(slice1, slice2 []TestCase) []TestCase {
+	seen := make(map[string]bool)
+	result := []TestCase{}
+
+	for _, val1 := range slice1 {
+		tn1 := val1.Package + "/" + string(val1.Test)
+		for _, val2 := range slice2 {
+			tn2 := val2.Package + "/" + string(val2.Test)
+			if tn1 == tn2 {
+				if _, ok := seen[tn1]; !ok {
+					seen[tn1] = true
+					result = append(result, val1)
+					break
+				}
+			}
+		}
+	}
+
+	return result
+}
+
+func testUnion(slice1, slice2 []*FailedTest) []*FailedTest {
+	seen := make(map[string]bool)
+	result := []*FailedTest{}
+
+	for _, val := range slice1 {
+		tn := val.Package + "/" + string(val.Test)
+		if _, ok := seen[tn]; !ok {
+			seen[tn] = true
+			result = append(result, val)
+		}
+	}
+
+	for _, val := range slice2 {
+		tn := val.Package + "/" + string(val.Test)
+		if _, ok := seen[tn]; !ok {
+			seen[tn] = true
+			result = append(result, val)
+		}
+	}
+
+	return result
+}
+
+func testNotIn(slice1, slice2 []*FailedTest) []*FailedTest {
+	mb := make(map[string]bool, len(slice1))
+	for _, val := range slice1 {
+		tn := val.Package + "/" + string(val.Test)
+		mb[tn] = true
+	}
+	var diff []*FailedTest
+	for _, val := range slice2 {
+		tn := val.Package + "/" + string(val.Test)
+		if _, found := mb[tn]; !found {
+			diff = append(diff, val)
+		}
+	}
+	return diff
+}
+
+func findFiles(root, needle string) []string {
+	files := []string{}
+	err := filepath.WalkDir(root, func(path string, info os.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+
+		l, _ := os.Stat(path)
+		if l.IsDir() && path != root {
+			files = append(files, findFiles(path, needle)...)
+		} else if strings.HasPrefix(filepath.Base(path), needle) {
+			files = append(files, path)
+		}
+
+		return nil
+	})
+
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	return files
+}
diff --git a/mod/wax/gotestsum/log.go b/mod/wax/gotestsum/log.go
new file mode 100644
index 00000000000..9b3c7872843
--- /dev/null
+++ b/mod/wax/gotestsum/log.go
@@ -0,0 +1,55 @@
+package gotestsum
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"os"
+
+	"gotest.tools/gotestsum/testjson"
+)
+
+func FromLogs(files ...string) (run *Run, err error) {
+	run = &Run{
+		Pipelines: []*Pipeline{},
+	}
+
+	for _, file := range files {
+		in, err := jsonlogReader(file)
+		if err != nil {
+			return nil, fmt.Errorf("failed to read jsonfile: %v", err)
+		}
+
+		defer func() {
+			err = errors.Join(in.Close(), err)
+		}()
+
+		exec, err := testjson.ScanTestOutput(testjson.ScanConfig{Stdout: in})
+		if err != nil {
+			return nil, fmt.Errorf("failed to scan testjson: %v", err)
+		}
+
+		pipeline := &Pipeline{}
+		pkgs := exec.Packages()
+		for _, pkg := range pkgs {
+			pipeline.Packages = append(pipeline.Packages, exec.Package(pkg))
+		}
+
+		run.Pipelines = append(run.Pipelines, pipeline)
+	}
+
+	return run, nil
+}
+
+func FindLogs(root, needle string) []string {
+	return findFiles(root, needle)
+}
+
+func jsonlogReader(v string) (io.ReadCloser, error) {
+	switch v {
+	case "", "-":
+		return io.NopCloser(os.Stdin), nil
+	default:
+		return os.Open(v)
+	}
+}
diff --git a/mod/wax/gotestsum/package.go b/mod/wax/gotestsum/package.go
new file mode 100644
index 00000000000..d111562a150
--- /dev/null
+++ b/mod/wax/gotestsum/package.go
@@ -0,0 +1,5 @@
+package gotestsum
+
+import "gotest.tools/gotestsum/testjson"
+
+type Package = testjson.Package
diff --git a/mod/wax/gotestsum/pipeline.go b/mod/wax/gotestsum/pipeline.go
new file mode 100644
index 00000000000..7c155375c98
--- /dev/null
+++ b/mod/wax/gotestsum/pipeline.go
@@ -0,0 +1,6 @@
+package gotestsum
+
+type Pipeline struct {
+	Packages []*Package
+	Metadata map[string]string
+}
diff --git a/mod/wax/gotestsum/run.go b/mod/wax/gotestsum/run.go
new file mode 100644
index 00000000000..121ff5f625a
--- /dev/null
+++ b/mod/wax/gotestsum/run.go
@@ -0,0 +1,133 @@
+package gotestsum
+
+type Run struct {
+	Pipelines []*Pipeline
+}
+
+func (r *Run) NeverRan() []TestCase {
+	var result []TestCase
+	for _, pipe := range r.Pipelines {
+		pipelineSkipped := []TestCase{}
+		for _, pkg := range pipe.Packages {
+			pipelineSkipped = append(pipelineSkipped, pkg.Skipped...)
+		}
+
+		if result == nil {
+			result = append(result, pipelineSkipped...)
+			continue
+		}
+
+		result = testTestIntersection(pipelineSkipped, result)
+	}
+
+	return result
+}
+
+func (r *Run) FailedAlways() []*FailedTest {
+	var result []*FailedTest
+	for _, pipe := range r.Pipelines {
+		pipelineFailed := []*FailedTest{}
+		for _, pkg := range pipe.Packages {
+			for _, testCase := range pkg.Failed {
+				ft := &FailedTest{
+					TestCase: testCase,
+					//nolint:staticcheck
+					Output:       pkg.Output(testCase.ID),
+					AlwaysFailed: true,
+				}
+				pipelineFailed = append(pipelineFailed, ft)
+			}
+		}
+		if result == nil {
+			result = append(result, pipelineFailed...)
+			continue
+		}
+
+		result = testIntersection(pipelineFailed, result)
+	}
+
+	return result
+}
+
+func (r *Run) FailedAtLeastOnceButNotAlways() []*FailedTest {
+	var result []*FailedTest
+	for _, pipe := range r.Pipelines {
+		pipelineFailed := []*FailedTest{}
+		for _, pkg := range pipe.Packages {
+			for _, testCase := range pkg.Failed {
+				ft := &FailedTest{
+					TestCase: testCase,
+					//nolint:staticcheck
+					Output: pkg.Output(testCase.ID),
+				}
+				pipelineFailed = append(pipelineFailed, ft)
+			}
+		}
+		if result == nil {
+			result = append(result, pipelineFailed...)
+			continue
+		}
+
+		result = testUnion(pipelineFailed, result)
+	}
+	always := r.FailedAlways()
+	result = testNotIn(always, result)
+
+	return result
+}
+
+//	func (r *Run) Slowest(num int) map[string]float64 {
+//		tests := map[string][]TestCase{}
+//		times := map[string]float64{}
+//		for _, pipe := range r.Pipelines {
+//			for _, pkg := range pipe.Packages {
+//				// Use only successful tests to calculate mean time
+//				for _, test := range pkg.Passed {
+//					fqn := test.Package + "/" + string(test.Test)
+//					val, ok := tests[fqn]
+//					if !ok {
+//						tests[fqn] = []TestCase{}
+//					}
+//					tests[fqn] = append(val, test)
+//				}
+//			}
+//		}
+//
+//		for fqn, val := range tests {
+//			var tm time.Duration
+//			for _, testCase := range val {
+//				tm += testCase.Elapsed
+//			}
+//			times[fqn] = math.Round(float64(tm.Microseconds()) / float64(len(val)))
+//		}
+//
+//		keys := make([]string, 0, len(times))
+//		for k := range times {
+//			keys = append(keys, k)
+//		}
+//
+//		sort.Slice(keys, func(i, j int) bool {
+//			return times[keys[i]] < times[keys[j]]
+//		})
+//
+//		if num <= len(keys) {
+//			keys = keys[:int(num)]
+//		}
+//		result := map[string]float64{}
+//		for _, key := range keys {
+//			result[key] = times[key]
+//		}
+//
+//		return result
+//	}
+
+//func (r *Run) OutputForTest(testCase *TestCase) {
+//	for _, pipe := range r.Pipelines {
+//		for _, pkg := range pipe.Packages {
+//			if testCase.Package == pkg {
+//
+//			}
+//		}
+//	}
+//	//pkg.OutputLines(pkg.Failed[0])
+//}
diff --git a/mod/wax/gotestsum/test.go b/mod/wax/gotestsum/test.go
new file mode 100644
index 00000000000..ad34f951b7a
--- /dev/null
+++ b/mod/wax/gotestsum/test.go
@@ -0,0 +1,12 @@
+package gotestsum
+
+import "gotest.tools/gotestsum/testjson"
+
+type TestCase = testjson.TestCase
+
+type FailedTest struct {
+	TestCase
+	KnownIssue   string
+	Output       string
+	AlwaysFailed bool
+}
diff --git a/mod/wax/hack/dev-setup-linux.sh b/mod/wax/hack/dev-setup-linux.sh
new file mode 100755
index 00000000000..652877dee43
--- /dev/null
+++ b/mod/wax/hack/dev-setup-linux.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+#   Copyright The containerd Authors.
+
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+
+#       http://www.apache.org/licenses/LICENSE-2.0
+
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+set -o errexit -o errtrace -o functrace -o nounset -o pipefail
+
+sudo apt-get install -qq --no-install-recommends golang make yamllint shellcheck
diff --git a/mod/wax/hack/dev-setup-macos.sh b/mod/wax/hack/dev-setup-macos.sh
new file mode 100755
index 00000000000..ce941fd5522
--- /dev/null
+++ b/mod/wax/hack/dev-setup-macos.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+#   Copyright The containerd Authors.
+
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+
+#       http://www.apache.org/licenses/LICENSE-2.0
+
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+set -o errexit -o errtrace -o functrace -o nounset -o pipefail
+
+brew install golang make yamllint shellcheck
diff --git a/mod/wax/hack/headers/bash.txt b/mod/wax/hack/headers/bash.txt
new file mode 100644
index 00000000000..191b8ced5c4
--- /dev/null
+++ b/mod/wax/hack/headers/bash.txt
@@ -0,0 +1,13 @@
+#   Copyright The containerd Authors.
+
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+
+#       http://www.apache.org/licenses/LICENSE-2.0
+
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
\ No newline at end of file
diff --git a/mod/wax/hack/headers/dockerfile.txt b/mod/wax/hack/headers/dockerfile.txt
new file mode 100644
index 00000000000..191b8ced5c4
--- /dev/null
+++ b/mod/wax/hack/headers/dockerfile.txt
@@ -0,0 +1,13 @@
+#   Copyright The containerd Authors.
+
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+
+#       http://www.apache.org/licenses/LICENSE-2.0
+
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
\ No newline at end of file
diff --git a/mod/wax/hack/headers/go.txt b/mod/wax/hack/headers/go.txt
new file mode 100644
index 00000000000..34c6611aadc
--- /dev/null
+++ b/mod/wax/hack/headers/go.txt
@@ -0,0 +1,15 @@
+/*
+   Copyright The containerd Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
\ No newline at end of file
diff --git a/mod/wax/hack/headers/makefile.txt b/mod/wax/hack/headers/makefile.txt
new file mode 100644
index 00000000000..dced09e47a8
--- /dev/null
+++ b/mod/wax/hack/headers/makefile.txt
@@ -0,0 +1,16 @@
+#   Copyright The containerd Authors.
+
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+
+#       http://www.apache.org/licenses/LICENSE-2.0
+
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+# project-checks is broken.
+# See https://github.com/containerd/nerdctl/pull/3889
\ No newline at end of file
diff --git a/mod/wax/report/annotation.go b/mod/wax/report/annotation.go
new file mode 100644
index 00000000000..dac86b4a417
--- /dev/null
+++ b/mod/wax/report/annotation.go
@@ -0,0 +1,110 @@
+package report
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/containerd/nerdctl/mod/wax/analyzer"
+	"github.com/containerd/nerdctl/mod/wax/format/action"
+	"github.com/containerd/nerdctl/mod/wax/gotestsum"
+)
+
+const (
+	testsNotRun        = "Tests that have NOT run in any pipeline"
+	testsNotRunComment = "If you authored these tests, you MUST look into this now, as this will block merging."
+
+	testsFailingKnown        = "Tests that are known to be problematic"
+	testsFailingKnownComment = "These tests are known to be flaky.\n" +
+		"Failures on these are probably not related to your changeset and can usually be ignored or retried."
+
+	testsFailingAll        = "These tests failed on ALL pipelines, and are NOT known to be problematic"
+	testsFailingAllComment = "These tests are not known to be problematic.\n" +
+		"The fact that they are failing on ALL pipelines is usually indicative that you broke something with your changeset." +
+		"You should look into this."
+
+	testsFailingSometimes        = "These tests failed at least once, and are NOT known to be problematic"
+	testsFailingSometimesComment = "These tests are not known to be problematic.\n" +
+		"If you did create these tests, then they are definitely flaky and you must look into it.\n" +
+		"If you did not create these tests, and do strongly believe this failure is unrelated to your changeset, " +
+		"please open a new ticket mentioning the test name in its title."
+)
+
+func Annotate(
+	root string,
+	version string,
+	commit string,
+	alwaysFailedTests []*gotestsum.FailedTest,
+	failedOnceTests []*gotestsum.FailedTest,
+	disabledTests []gotestsum.TestCase,
+	headfile string) {
+
+	strip := root
+	if version != "" {
+		strip += "/" + version
+	}
+
+	title := ""
+	message := ""
+
+	list := ""
+	for _, t := range disabledTests {
+		locFile, locLine, locEndLine := analyzer.FindLocation(string(t.Test), t.Package, strip)
+		link := analyzer.LocationURL(commit, locFile, locLine, locEndLine, root)
+		list += fmt.Sprintf("- [%s](%s)\n", t.Test, link)
+	}
+
+	if list != "" {
+		title = testsNotRun
+		message = fmt.Sprintf("> %s\n\n%s", testsNotRunComment, list)
+		action.Error(os.Stdout, headfile, 1, 1, 0, 0, title, message)
+	}
+
+	known := ""
+	unknownFailedAll := ""
+	unknownFailedOnce := ""
+	for _, t := range alwaysFailedTests {
+		locFile, locLine, locEndLine := analyzer.FindLocation(string(t.Test), t.Package, strip)
+		link := analyzer.LocationURL(commit, locFile, locLine, locEndLine, root)
+		if t.KnownIssue != "" {
+			spl := strings.Split(t.KnownIssue, "/")
+			id := spl[len(spl)-1]
+			known += fmt.Sprintf("- [%s](%s): see issue #%s\n", t.Test, link, id)
+		} else {
+			unknownFailedAll += fmt.Sprintf("- [%s](%s)\n", t.Test, link)
+			action.Error(os.Stdout, locFile, locLine, locEndLine, 0, 0, "Test failed on all targets", "\n```\n"+strings.ReplaceAll(t.Output, "\n", "\n")+"\n```")
+		}
+	}
+
+	for _, t := range failedOnceTests {
+		locFile, locLine, locEndLine := analyzer.FindLocation(string(t.Test), t.Package, strip)
+		link := analyzer.LocationURL(commit, locFile, locLine, locEndLine, root)
+		if t.KnownIssue != "" {
+			spl := strings.Split(t.KnownIssue, "/")
+			id := spl[len(spl)-1]
+			known += fmt.Sprintf("- [%s](%s): see issue #%s\n", t.Test, link, id)
+		} else {
+			unknownFailedOnce += fmt.Sprintf("- [%s](%s)\n", t.Test, link)
+			action.Error(os.Stdout, locFile, locLine, locEndLine, 0, 0, "Test failed in a flaky way", "\n```\n"+strings.ReplaceAll(t.Output, "\n", "\n")+"\n```")
+		}
+	}
+
+	if known != "" {
+		title = testsFailingKnown
+		message = fmt.Sprintf("> %s\n\n%s\n", testsFailingKnownComment, known)
+		action.Warning(os.Stdout, headfile, 1, 1, 0, 0, title, message)
+	}
+
+	if unknownFailedAll != "" {
+		title = testsFailingAll
+		message = fmt.Sprintf("> %s\n\n%s\n", testsFailingAllComment, unknownFailedAll)
+		action.Error(os.Stdout, headfile, 1, 1, 0, 0, title, message)
+	}
+
+	if unknownFailedOnce != "" {
+		title = testsFailingSometimes
+		message = fmt.Sprintf("> %s\n\n%s\n", testsFailingSometimesComment, unknownFailedOnce)
+		action.Error(os.Stdout, headfile, 1, 1, 0, 0, title, message)
+	}
+
+}
diff --git a/mod/wax/scripts/collector.sh b/mod/wax/scripts/collector.sh
new file mode 100755
index 00000000000..24405d22bab
--- /dev/null
+++ b/mod/wax/scripts/collector.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+
+#   Copyright The containerd Authors.
+
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+
+#       http://www.apache.org/licenses/LICENSE-2.0
+
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+# collector.sh provides methods to collect all system logs relevant for debugging.
+# It is primarily intended to be used after a run of gotestsum has completed (successfully or not).
+
+# shellcheck disable=SC2034,SC2015
+set -o errexit -o errtrace -o functrace -o nounset -o pipefail
+
+# collect::logs::units retrieve all running units and gather their logs
+collect::logs::systemctl(){
+  local destination="$1"
+  local item
+  local args=(--no-pager)
+  if command -v systemctl >/dev/null; then
+    [ "$(id -u)" == 0 ] || args+=(--user)
+    for item in $(systemctl show '*' --state=running --property=Id --value "${args[@]}" | grep . | sort | uniq); do
+      journalctl "${args[@]}" -u "$item" > "$destination/systemd-$item.log"
+    done
+  fi
+}
+
+collect::logs(){
+  collect::logs::systemctl "$@"
+}
+
+collect::metadata(){
+  local item
+  local key
+  local value
+  local sep=""
+
+  printf "{"
+  for item in "$@"; do
+    printf "  %s\"%s\": \"%s\"" "$sep" "${item%=*}" "${item#*=}"
+    sep=","$'\n'
+  done
+  printf "}"
+}