diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 256341f..8cc21ab 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -53,39 +53,19 @@ jobs:
go-version-file: go.mod
cache: true
- - name: Set up Task
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
-
# `task lint` and `task test`, and not a list of steps: a second definition of what
# the gate is, maintained in a workflow file, goes out of step with the first exactly
# when a check is added.
- name: Lint
- run: task lint
+ run: go tool task lint
- name: Test the machine definition
- run: task test
+ run: go tool task test
# always(): a failed suite is when the per-function numbers are wanted.
- name: Coverage summary
if: always()
- run: |
- if [ ! -f coverage.out ]; then
- echo "coverage.out was not produced; the tests may have failed before coverage ran." >> "$GITHUB_STEP_SUMMARY"
- exit 0
- fi
- {
- echo "## Go coverage"
- echo
- echo "**$(go tool cover -func=coverage.out | awk '/^total:/ {print $3}')** total"
- echo
- echo 'Function detail
'
- echo
- echo '```'
- go tool cover -func=coverage.out
- echo '```'
- echo ' '
- } >> "$GITHUB_STEP_SUMMARY"
- go tool cover -html=coverage.out -o coverage.html
+ run: go tool task coverage
- name: Upload Go coverage HTML
if: always()
@@ -100,23 +80,13 @@ jobs:
# is against what it merges into, on a push against what was there before.
- name: What this change broke, and whether a test noticed
env:
- EVENT: ${{ github.event_name }}
- BASE_REF: ${{ github.base_ref }}
BEFORE: ${{ github.event.before }}
- run: |
- set -euo pipefail
- if [ "$EVENT" = "pull_request" ]; then
- base="origin/$BASE_REF"
- else
- base="$BEFORE"
- fi
- git rev-parse --verify "$base^{commit}" >/dev/null 2>&1 || base=HEAD~1
- task mutate BASE="$base"
+ run: go tool task mutate
# The steps below run spin-machine, so it has to exist. `task test` exercises the
# machine package; only a build catches what the command around it does with it.
- name: Build the tools
- run: task tools
+ run: go tool task tools
# `task test` proves the machine package agrees with itself. The steps below ask the
# QEMU binary, which is the half that can disagree: the device set is decided in
@@ -148,7 +118,7 @@ jobs:
- name: Fetch the pinned QEMU
id: qemu
continue-on-error: true
- run: task qemu:fetch
+ run: go tool task qemu:fetch
# vhost-vsock is a kernel module, and the machine's vsock device cannot be created
# without /dev/vhost-vsock. The runner has the node and gives the user no access to
@@ -158,12 +128,8 @@ jobs:
# told what went unchecked instead of being shown a pass.
- name: Load vhost_vsock
if: steps.qemu.outcome == 'success'
- run: |
- # The device first: a kernel that builds it in - Spin OS's, which has no modules - has
- # it from boot, and modprobe there has nothing to find.
- { [ -c /dev/vhost-vsock ] || sudo modprobe vhost_vsock; } && sudo chmod 0666 /dev/vhost-vsock ||
- echo "no vhost_vsock here; the check will name vhost-vsock-pci as unchecked"
+ run: go tool task vsock
- name: Check QEMU accepts the machine's arguments
if: steps.qemu.outcome == 'success'
- run: task verify:args
+ run: go tool task verify:args
diff --git a/.github/workflows/compat.yml b/.github/workflows/compat.yml
index 7b04bbf..9b6f12b 100644
--- a/.github/workflows/compat.yml
+++ b/.github/workflows/compat.yml
@@ -63,37 +63,21 @@ jobs:
go-version-file: go.mod
cache: false
- - name: Set up Task
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
-
# A builder that can export its cache to the directory above; the runner keeps it.
- name: A BuildKit that keeps its cache
- run: docker buildx inspect compat > /dev/null 2>&1 && docker buildx use compat || docker buildx create --use --name compat --driver docker-container
+ run: go tool task boot:buildkit
- name: Build this change's QEMU and kernel, and the tools
- run: task qemu:build kernel:build tools
+ run: go tool task qemu:build kernel:build tools
# The newest release, checked against its sha256 and its signature, as the machine before;
- # this change's machine is the same tree with what was just built in place of its own.
+ # this change's machine is the same tree with what was just built in place of its own. The
+ # steps after boot that tree: it is their SPIN_MACHINE_OUTPUT.
- name: The machine before, and this one
- run: |
- set -euo pipefail
- task boot:previous
- previous=$(cat _output/compat/previous)
- here=$PWD/_output/compat/here
- rm -rf "$here" && cp -as "$previous" "$here"
- rm -rf "$here/bin" "$here/qemu" "$here/kernel"
- mkdir -p "$here/bin" "$here/kernel"
- cp _output/bin/qemu-system-x86_64 _output/bin/qemu-img _output/bin/spin-machine "$here/bin/"
- cp -r _output/qemu "$here/qemu"
- cp _output/kernel/vmlinux "$here/kernel/vmlinux"
- {
- echo "SPIN_COMPAT_PREVIOUS=$previous"
- echo "SPIN_MACHINE_OUTPUT=$here"
- } >> "$GITHUB_ENV"
+ run: go tool task boot:here
- name: Every ROM the machine maps is the fingerprint's
- run: SPIN_ROM_TEST=1 go test ./boot/ -run '^TestEveryROMIsTheFingerprints$' -count=1 -v -timeout 3m
+ run: go tool task boot:roms
- name: A checkpoint of the release before resumes here
- run: go test ./boot/ -run '^TestACheckpointOfTheReleaseBeforeResumesHere$' -count=1 -v -timeout 10m
+ run: go tool task boot:compat
diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml
index 307d37b..d3d7ca6 100644
--- a/.github/workflows/image.yml
+++ b/.github/workflows/image.yml
@@ -60,10 +60,7 @@ jobs:
id: touches
env:
BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
- run: |
- build=$(hack/touches "$BASE" alpine e2fsprogs ubuntu mkosi task git-lfs buildkit -- \
- image e2fsprogs go.mod .github/workflows/image.yml)
- echo "build=${build}" | tee -a "$GITHUB_OUTPUT"
+ run: go tool task touches -- alpine e2fsprogs ubuntu mkosi task git-lfs buildkit -- image e2fsprogs go.mod .github/workflows/image.yml
build:
needs: changed
@@ -81,9 +78,6 @@ jobs:
go-version-file: go.mod
cache: true
- - name: Set up Task
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
-
# One sticky disk per thing built, and no silent fallback; see the note in qemu.yml. This
# job builds e2fsprogs and the image builder, which is one job and so one disk.
- name: Setup Blacksmith Builder
@@ -100,8 +94,10 @@ jobs:
# The image is made and checked with the static e2fsprogs this repository builds, not
# the build container's, and image/build.sh stops when it is not there.
- name: Build e2fsprogs
- run: |
- CACHE_EXPORT=${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} CACHE_BACKEND=gha task e2fsprogs:build
+ env:
+ CACHE_BACKEND: gha
+ CACHE_EXPORT: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
+ run: go tool task e2fsprogs:build
# image/build.sh asserts what came out on the finished filesystem rather than
# trusting the build that made it: that there is an /sbin/init and a /bin/sh, that
@@ -114,13 +110,13 @@ jobs:
# it would buy minutes of cache traffic in exchange for minutes of archive traffic,
# and a cache entry large enough to push others out of the repository's 10 GB.
- name: Build the base image
- run: |
- CACHE_EXPORT=${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} CACHE_BACKEND=gha task image:build
+ env:
+ CACHE_BACKEND: gha
+ CACHE_EXPORT: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
+ run: go tool task image:build
- name: What this image is
- run: |
- ls -la _output/image/rootfs.qcow2
- echo "sha256: $(sha256sum _output/image/rootfs.qcow2 | cut -d' ' -f1)"
+ run: go tool task image:summary
# Not uploaded as an artifact: it is ~860 MB and the release workflow rebuilds it
# from the same inputs. What this job is for is finding out that it still builds and
diff --git a/.github/workflows/kernel.yml b/.github/workflows/kernel.yml
index 2568151..65d1ed9 100644
--- a/.github/workflows/kernel.yml
+++ b/.github/workflows/kernel.yml
@@ -57,10 +57,7 @@ jobs:
id: touches
env:
BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
- run: |
- build=$(hack/touches "$BASE" debian debian-snapshot kernel moby-check-config -- \
- kernel go.mod .github/workflows/kernel.yml)
- echo "build=${build}" | tee -a "$GITHUB_OUTPUT"
+ run: go tool task touches -- debian debian-snapshot kernel moby-check-config -- kernel go.mod .github/workflows/kernel.yml
build:
needs: changed
@@ -78,9 +75,6 @@ jobs:
go-version-file: go.mod
cache: true
- - name: Set up Task
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
-
# One sticky disk per thing built, and no silent fallback; see the note in qemu.yml.
- name: Setup Blacksmith Builder
uses: useblacksmith/setup-docker-builder@19215110ab936351210feebdfa5b440b4493e184 # v2.2.0
@@ -97,20 +91,17 @@ jobs:
# survived the strip. Without them QEMU has no entry point into this kernel and the
# failure is a VM that does not start, in whichever lane boots one next.
- name: Build the kernel
- run: |
- CACHE_EXPORT=${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} CACHE_BACKEND=gha task kernel:build KERNEL_NPROC=4
+ env:
+ CACHE_BACKEND: gha
+ CACHE_EXPORT: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
+ run: go tool task kernel:build KERNEL_NPROC=4
# What it produced, on the run's summary: a pull request that changes the config, or
# bumps the Debian toolchain the kernel is compiled with, changes every guest's next
# boot, and the change that does it need not look like much. Checkpoints are not
# affected: the kernel is not in the fingerprint (machine.Spec.Fingerprint).
- name: What this kernel is
- run: |
- set -euo pipefail
- {
- echo "kernel sha256: $(sha256sum _output/kernel/vmlinux | cut -d' ' -f1)"
- echo "config sha256: $(sha256sum _output/kernel/kernel-config | cut -d' ' -f1)"
- } | tee -a "$GITHUB_STEP_SUMMARY"
+ run: go tool task kernel:summary
- name: Upload the kernel
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
diff --git a/.github/workflows/lab.yml b/.github/workflows/lab.yml
index 5287d25..05cbaa6 100644
--- a/.github/workflows/lab.yml
+++ b/.github/workflows/lab.yml
@@ -89,6 +89,9 @@ jobs:
KERNEL_RUN_C: ${{ inputs.kernel_run_c }}
QEMU_RUN: ${{ inputs.qemu_run }}
VARS: ${{ inputs.vars }}
+ # GitHub reads a workflow command (::error::) only at the start of a line, where Task's
+ # prefixed output puts the task's name.
+ TASK_OUTPUT: interleaved
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -101,39 +104,14 @@ jobs:
go-version-file: go.mod
cache: false
- - name: Set up Task
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
-
- name: Unpack the release
- run: |
- set -euo pipefail
- if [ -z "$RELEASE" ]; then
- RELEASE=$(gh release list -R "$GITHUB_REPOSITORY" --exclude-drafts --exclude-pre-releases --limit 1 --json tagName -q '.[0].tagName')
- fi
- rm -rf _output/lab
- mkdir -p _output/lab/tmp
- gh release download "$RELEASE" -R "$GITHUB_REPOSITORY" -p '*.tar.gz' -D _output/lab
- tar -xzf _output/lab/*.tar.gz -C _output/lab
- {
- echo "RELEASE=$RELEASE"
- echo "SPIN_MACHINE_OUTPUT=$PWD/_output/lab/spin-machine-${RELEASE}-linux-x86_64/usr/share/spin-stack"
- # On disk: the overlays and memory files go here, gigabytes a tmpfs /tmp holds in RAM.
- echo "TMPDIR=$PWD/_output/lab/tmp"
- } >> "$GITHUB_ENV"
+ run: go tool task lab:unpack
# Another build to compare: a Kernel run's vmlinux (B, and C beside it) or a QEMU run's
# QEMU and firmware. qemu.yml and kernel.yml build any branch on dispatch.
- name: Fetch the builds to compare
if: inputs.kernel_run != '' || inputs.kernel_run_c != '' || inputs.qemu_run != ''
- run: |
- set -euo pipefail
- fetch() { # run, artifact pattern, directory
- [[ "$1" =~ ^[0-9]+$ ]] || { echo "::error::a run id is a number, got: $1"; exit 1; }
- gh run download "$1" -R "$GITHUB_REPOSITORY" -p "$2" -D "$3"
- }
- if [ -n "$KERNEL_RUN" ]; then fetch "$KERNEL_RUN" 'vmlinux-*' _output/lab/kernel-b; fi
- if [ -n "$KERNEL_RUN_C" ]; then fetch "$KERNEL_RUN_C" 'vmlinux-*' _output/lab/kernel-c; fi
- if [ -n "$QEMU_RUN" ]; then fetch "$QEMU_RUN" 'qemu-*-linux-amd64' _output/lab/qemu-b; fi
+ run: go tool task lab:fetch
# beside: the build is booted interleaved with the release's own - SPIN_KERNEL_B and
# SPIN_KERNEL_C for a kernel, and for QEMU a second release tree, SPIN_MACHINE_OUTPUT_B,
@@ -145,94 +123,23 @@ jobs:
if: inputs.kernel_run != '' || inputs.kernel_run_c != '' || inputs.qemu_run != ''
env:
VARIANT: ${{ inputs.variant }}
- run: |
- set -euo pipefail
- one() { # the one file under $1 whose path matches $2
- f=$(find "$1" -path "$2" -type f | head -1)
- [ -n "$f" ] || { echo "::error::$1 has no $2"; exit 1; }
- echo "$PWD/$f"
- }
- b=$PWD/_output/lab/release-b
- tree_b() { [ -d "$b" ] || cp -as "$SPIN_MACHINE_OUTPUT" "$b"; }
- if [ -n "$KERNEL_RUN" ]; then
- k=$(one _output/lab/kernel-b '*/vmlinux')
- if [ "$VARIANT" = instead ]; then
- tree_b; rm -f "$b/kernel/vmlinux"; install -m 0644 "$k" "$b/kernel/vmlinux"
- else
- echo "SPIN_KERNEL_B=$k" >> "$GITHUB_ENV"
- fi
- fi
- if [ -n "$KERNEL_RUN_C" ]; then
- [ "$VARIANT" = beside ] || { echo "::error::kernel_run_c is a third kernel beside the release, not one to boot instead"; exit 1; }
- echo "SPIN_KERNEL_C=$(one _output/lab/kernel-c '*/vmlinux')" >> "$GITHUB_ENV"
- fi
- if [ -n "$QEMU_RUN" ]; then
- q=$(one _output/lab/qemu-b '*/bin/qemu-system-x86_64')
- tree_b; rm -rf "$b/bin/qemu-system-x86_64" "$b/qemu"
- install -m 0755 "$q" "$b/bin/qemu-system-x86_64"
- cp -r "$(dirname "$(dirname "$q")")/qemu" "$b/qemu"
- [ "$VARIANT" = instead ] || echo "SPIN_MACHINE_OUTPUT_B=$b" >> "$GITHUB_ENV"
- fi
- if [ "$VARIANT" = instead ]; then echo "SPIN_MACHINE_OUTPUT=$b" >> "$GITHUB_ENV"; fi
+ run: go tool task lab:place
- name: Take the variables
- run: |
- set -euo pipefail
- IFS=';' read -ra pairs <<< "$VARS"
- for pair in "${pairs[@]}"; do
- pair="${pair#"${pair%%[![:space:]]*}"}"
- pair="${pair%"${pair##*[![:space:]]}"}"
- [ -z "$pair" ] && continue
- key="${pair%%=*}"
- [[ "$pair" == *=* && "$key" =~ ^(REPS|TOP|GAP|ONLY|FLAGS|CPUS|MEMORY_MIB|DEBUG|FILE_MB|CACHE_TIMEOUT_S|GRAPH_[A-Z_]+|SHARE_[A-Z_]+|SPIN_[A-Z0-9_]+)$ ]] ||
- { echo "::error::not a variable this workflow passes on: $pair"; exit 1; }
- case "$key" in
- SPIN_MACHINE_OUTPUT|SPIN_MACHINE_OUTPUT_B|SPIN_KERNEL_B|SPIN_KERNEL_C) echo "::error::$key is set by this workflow"; exit 1 ;;
- esac
- echo "$pair" >> "$GITHUB_ENV"
- done
+ run: go tool task lab:vars
- name: What this run is
env:
VARIANT: ${{ inputs.variant }}
- run: |
- {
- echo "### $EXPERIMENT on $RUNNER_NAME"
- echo
- echo "- release: $RELEASE"
- echo "- kernel B: ${KERNEL_RUN:+run }${KERNEL_RUN:-none}"
- echo "- kernel C: ${KERNEL_RUN_C:+run }${KERNEL_RUN_C:-none}"
- echo "- QEMU B: ${QEMU_RUN:+run }${QEMU_RUN:-none}"
- if [ -n "$KERNEL_RUN$KERNEL_RUN_C$QEMU_RUN" ]; then echo "- booted: $VARIANT the release"; fi
- echo "- vars: ${VARS:-none}"
- echo "- CPU: $(sed -n 's/^model name\s*: //p' /proc/cpuinfo | head -1), $(nproc) CPUs"
- echo "- load: $(cut -d' ' -f1-3 /proc/loadavg)"
- if sudo -n true 2>/dev/null; then echo "- sudo -n: yes"; else echo "- sudo -n: **no** (targets that edit an overlay skip)"; fi
- } | tee -a "$GITHUB_STEP_SUMMARY"
- [ -r /dev/kvm ] && [ -w /dev/kvm ] || { echo "::error::no usable /dev/kvm"; exit 1; }
+ run: go tool task lab:about
# report writes its JSON where OUT says; the others print their tables through t.Log.
- # A skipped test exits 0, and a green run that measured nothing reads as a result, so a
- # skip fails the run with the test's reason.
- name: Run the experiment
- run: |
- set -o pipefail
- task "$EXPERIMENT" OUTPUT_DIR="$SPIN_MACHINE_OUTPUT" OUT="$PWD/_output/lab/report.json" 2>&1 | tee _output/lab/lab.log
- if grep -q -- '--- SKIP' _output/lab/lab.log; then
- echo "::error::the experiment skipped: $(grep -B1 -- '--- SKIP' _output/lab/lab.log | head -1 | sed 's/^.*_test.go:[0-9]*: //')"
- exit 1
- fi
+ run: go tool task lab:run
- name: What it found
if: always()
- run: |
- [ -f _output/lab/lab.log ] || exit 0
- {
- echo
- echo '```'
- grep -E '^\s{4}\S' _output/lab/lab.log | sed 's/^ //' | tail -300
- echo '```'
- } >> "$GITHUB_STEP_SUMMARY"
+ run: go tool task lab:found
- name: Keep the log
if: always()
diff --git a/.github/workflows/osv.yml b/.github/workflows/osv.yml
index 49051e3..af43128 100644
--- a/.github/workflows/osv.yml
+++ b/.github/workflows/osv.yml
@@ -31,20 +31,17 @@ jobs:
go-version-file: go.mod
cache: true
- - name: Set up Task
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
-
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Scan the newest release
env:
GH_TOKEN: ${{ github.token }}
- run: task osv
+ run: go tool task osv
# One issue a package, which a release closes once it has no finding in it; hack/osvissues
# says why a package and not a finding.
- name: Bring the issues to the findings
env:
GH_TOKEN: ${{ github.token }}
- run: go run ./hack/osvissues -release "$(gh release view --json tagName -q .tagName)" -findings _output/osv.tsv
+ run: go tool task osv:issues
diff --git a/.github/workflows/qemu.yml b/.github/workflows/qemu.yml
index b089983..b6327ce 100644
--- a/.github/workflows/qemu.yml
+++ b/.github/workflows/qemu.yml
@@ -67,10 +67,7 @@ jobs:
id: touches
env:
BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
- run: |
- build=$(hack/touches "$BASE" alpine qemu debian debian-snapshot qboot -- \
- qemu go.mod .github/workflows/qemu.yml)
- echo "build=${build}" | tee -a "$GITHUB_OUTPUT"
+ run: go tool task touches -- alpine qemu debian debian-snapshot qboot -- qemu go.mod .github/workflows/qemu.yml
build:
needs: changed
@@ -85,9 +82,6 @@ jobs:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- - name: Set up Task
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
-
# For the acceptance check at the end, which is a Go test. The runner's own Go is
# whatever the image happens to carry, and go.mod names the one this builds with.
- name: Set up Go
@@ -135,11 +129,13 @@ jobs:
# to bump is how CI and production end up on different QEMUs.
- name: Read the pinned QEMU version
id: qemu
- run: echo "version=$(go tool versions version qemu)" >> "$GITHUB_OUTPUT"
+ run: go tool task qemu:version
- name: Build QEMU and verify what came out
- run: |
- CACHE_EXPORT=${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} CACHE_BACKEND=gha task qemu:build QEMU_JOBS=4
+ env:
+ CACHE_BACKEND: gha
+ CACHE_EXPORT: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
+ run: go tool task qemu:build QEMU_JOBS=4
# vhost-vsock is a kernel module, and the machine's vsock device cannot be created
# without /dev/vhost-vsock. The runner has the node and gives the user no access to
@@ -148,11 +144,7 @@ jobs:
# keeps the device line and drops the requirement, and a runner without it is told
# what went unchecked instead of being shown a pass.
- name: Load vhost_vsock
- run: |
- # The device first: a kernel that builds it in - Spin OS's, which has no modules - has
- # it from boot, and modprobe there has nothing to find.
- { [ -c /dev/vhost-vsock ] || sudo modprobe vhost_vsock; } && sudo chmod 0666 /dev/vhost-vsock ||
- echo "no vhost_vsock here; the check will name vhost-vsock-pci as unchecked"
+ run: go tool task vsock
# The other half of what a device allowlist decides. `task qemu:build` ends in
# qemu:verify, which asks this binary which accelerators and firmware files it has;
@@ -164,7 +156,7 @@ jobs:
# lane that catches a change made on *this* side, before the binary it would break is
# published at all.
- name: Check the built QEMU accepts the machine's arguments
- run: task verify:args
+ run: go tool task verify:args
# What it produced, and whether it still offers the machine type every checkpoint is
# saved on, on the run's summary. A changed QEMU binary is a new emulator of the same
@@ -172,13 +164,7 @@ jobs:
# checkpoint the released QEMU saved resumes on it is boot:compat's to say, on the KVM
# runner (compat.yml).
- name: What this QEMU is
- run: |
- set -euo pipefail
- {
- echo "qemu sha256: $(sha256sum _output/bin/qemu-system-x86_64 | cut -d' ' -f1)"
- echo "qboot.bin: $(stat -c%s _output/qemu/qboot.bin) bytes"
- go run ./hack/machinetype check -qemu _output/bin/qemu-system-x86_64
- } | tee -a "$GITHUB_STEP_SUMMARY"
+ run: go tool task qemu:summary
- name: Upload the extracted binaries
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
@@ -208,15 +194,4 @@ jobs:
# runner's sticky disk already holds these layers.
- name: Publish the runtime image
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
- run: |
- docker buildx build \
- --file qemu/Dockerfile \
- --target runtime \
- --platform linux/amd64 \
- --cache-from type=gha,scope=qemu \
- $(go tool versions args alpine qemu debian debian-snapshot qboot) \
- --build-arg JOBS=4 \
- --label org.opencontainers.image.source=https://github.com/${{ github.repository }} \
- --tag ghcr.io/${{ github.repository }}/qemu:${{ steps.qemu.outputs.version }} \
- --push \
- .
+ run: go tool task qemu:publish
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8e451f4..6a48f93 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -56,6 +56,10 @@ jobs:
timeout-minutes: 180
outputs:
version: ${{ steps.version.outputs.version }}
+ env:
+ # GitHub reads a workflow command (::error::) only at the start of a line, where Task's
+ # prefixed output puts the task's name.
+ TASK_OUTPUT: interleaved
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -70,9 +74,6 @@ jobs:
go-version-file: go.mod
cache: true
- - name: Set up Task
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
-
# Its own sticky disk, and not one of the lanes'. This job builds all four artefacts, so
# it would want every lane's disk at once and can only have one — and a release can
# overlap a lane, which sharing a key would make them contend for. The BuildKit scopes it
@@ -103,28 +104,7 @@ jobs:
id: version
env:
INPUT_VERSION: ${{ inputs.version }}
- run: |
- set -euo pipefail
- if [ -n "${INPUT_VERSION:-}" ]; then
- version="$INPUT_VERSION"
- origin="given by the caller"
- else
- day=$(date -u +%Y%m%d)
- # Count what today already has. `git tag -l` on a fetch-depth:0 checkout sees
- # every tag; the sequence is two digits so that v20260909.02 sorts after .01
- # and both sort before v20260910.01, as strings, everywhere.
- taken=$(git tag -l "v${day}.*" | wc -l)
- version=$(printf 'v%s.%02d' "$day" "$((taken + 1))")
- origin="generated for today (${taken} already taken)"
- fi
-
- case "$version" in
- v[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].[0-9][0-9]) ;;
- *) echo "::error::'$version' is not vYYYYMMDD.NN" >&2; exit 1 ;;
- esac
-
- echo "version=$version" >> "$GITHUB_OUTPUT"
- echo "Releasing $version — $origin"
+ run: go tool task release:version
# `task release` builds all four artefacts and packs them, ending in hack/release,
# which refuses to produce a tarball with anything missing. A release that installs
@@ -140,7 +120,7 @@ jobs:
# Reads what main's lanes exported and writes nothing back: a release is built once, and
# the next one reads main's newer cache, not this. See CACHE_EXPORT in Taskfile.yml.
CACHE_EXPORT: "false"
- run: task release QEMU_JOBS=4 KERNEL_NPROC=4
+ run: go tool task release QEMU_JOBS=4 KERNEL_NPROC=4
# The machine's identity, and what it costs whoever holds checkpoints.
#
@@ -158,50 +138,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
- run: |
- set -euo pipefail
- name="spin-machine-${VERSION}-linux-x86_64"
- manifest="_output/release/${name}/usr/share/spin-stack/machine.env"
- cat "$manifest"
-
- # The release before this one, for both the verdict and the notes: the newest published
- # one that is not this version, so a re-run of a version that already published compares
- # against its predecessor rather than against itself. A listing that fails fails the
- # step - read as "no releases", it would announce a first release that is not one.
- releases=$(gh release list -R "$GITHUB_REPOSITORY" --limit 30 --json tagName -q '.[].tagName')
- previous=$(printf '%s\n' "$releases" | grep -vFx -- "$VERSION" | head -1 || true)
-
- if [ -n "$previous" ]; then
- # machine.env's lines are the script's key=value arguments, as they are.
- # shellcheck disable=SC2046
- REFERENCE="$previous" hack/fingerprint-diff $(grep '_sha256=' "$manifest") > /tmp/verdict.md
- # What reached the machine since: the commits by the part they changed, the pins
- # that moved, the kernel options and the patches, read from git.
- go run ./hack/releasenotes -from "$previous" -to HEAD > /tmp/changes.md
- else
- echo "Nothing to compare against: this is the first published release." > /tmp/verdict.md
- echo "The first release: nothing before it to say what changed against." > /tmp/changes.md
- fi
- # Whether the QEMU shipped offers a machine type newer than the one every checkpoint is
- # saved on: said, never acted on (machine.MachineType).
- offer=$(go run ./hack/machinetype check -qemu "_output/release/${name}/usr/share/spin-stack/bin/qemu-system-x86_64")
- if [ -n "$offer" ]; then printf '\n%s\n' "$offer" >> /tmp/verdict.md; fi
- cat /tmp/verdict.md
- cat /tmp/changes.md
-
- {
- echo 'notes<> "$GITHUB_OUTPUT"
+ run: go tool task release:notes
# v4 is the one that installs cosign v3. Dependabot moves this pin like every other `uses:`.
- name: Install cosign
@@ -213,51 +150,22 @@ jobs:
- name: Write the provenance
env:
VERSION: ${{ steps.version.outputs.version }}
- run: go tool provenance -version "${VERSION}" -out _output/provenance.json
+ run: go tool task release:provenance
# Each file is signed and attested, then each bundle checked against the identity that made
# it, file by file: a bundle nothing verifies is a step that can quietly stop working.
- #
- # An attestation is checked by the file's digest, not the file: verify-blob-attestation reads
- # a blob whole and refuses one over COSIGN_MAX_ATTACHMENT_SIZE (128 MiB), and the tarball is
- # 338 MB (v20261013.01's run failed there). The digest is what the in-toto subject names.
- name: Sign and attest what the release offers
env:
IDENTITY: ${{ github.server_url }}/${{ github.repository }}/.github/workflows/release.yml@${{ github.ref }}
- run: |
- set -euo pipefail
- attested() {
- cosign verify-blob-attestation --type "$1" --bundle "$2" \
- --digest "$(sha256sum "$3" | cut -d' ' -f1)" --digestAlg sha256 \
- --certificate-identity "${IDENTITY}" \
- --certificate-oidc-issuer https://token.actions.githubusercontent.com
- }
- for f in _output/release/*.tar.gz _output/release/*.tar.gz.sha256 _output/release/machine.env _output/release/*.cdx.json; do
- cosign sign-blob --yes --bundle "${f}.sigstore.json" "${f}" > /dev/null
- cosign attest-blob --yes --type slsaprovenance1 --predicate _output/provenance.json \
- --bundle "${f}.provenance.sigstore.json" "${f}" > /dev/null
- cosign verify-blob --bundle "${f}.sigstore.json" --certificate-identity "${IDENTITY}" \
- --certificate-oidc-issuer https://token.actions.githubusercontent.com "${f}"
- attested slsaprovenance1 "${f}.provenance.sigstore.json" "${f}"
- done
- # The tarball's SBOM (hack/release), attested over it: what it is made of, by the
- # workflow that built it. The same SBOM is inside it, at usr/share/spin-stack/sbom.
- for f in _output/release/*.tar.gz; do
- cosign attest-blob --yes --type cyclonedx --predicate "${f%.tar.gz}.cdx.json" \
- --bundle "${f}.cdx.sigstore.json" "${f}" > /dev/null
- attested cyclonedx "${f}.cdx.sigstore.json" "${f}"
- done
- go tool attestgate files -require provenance _output/release
- go tool attestgate files -require cdx -except '*.sha256,machine.env,*.cdx.json' _output/release
+ run: go tool task release:sign
# Tagged here, after the files are signed and before they are published, so that the next
# run's count of "tags already taken today" is right and the tarball has a commit behind it.
# A version tagged already is refused here: git push does not move a tag.
- name: Tag the commit
- run: |
- set -euo pipefail
- git tag "${{ steps.version.outputs.version }}"
- git push origin "${{ steps.version.outputs.version }}"
+ env:
+ VERSION: ${{ steps.version.outputs.version }}
+ run: go tool task release:tag
- name: Publish
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
diff --git a/.github/workflows/report.yml b/.github/workflows/report.yml
index e4fc574..3c7e59e 100644
--- a/.github/workflows/report.yml
+++ b/.github/workflows/report.yml
@@ -41,6 +41,9 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
+ # GitHub reads a workflow command (::error::) only at the start of a line, where Task's
+ # prefixed output puts the task's name.
+ TASK_OUTPUT: interleaved
steps:
# The harness from the ref asked for - the release's own tag when release.yml calls - and,
# run by hand, from where the run was started: a release older than the report has none.
@@ -49,29 +52,6 @@ jobs:
with:
ref: ${{ inputs.ref || github.sha }}
- # What this runner can do, said before anything is measured: a row the report skips is one
- # this host lacks the device for, and the summary says which before the report does.
- - name: What this runner has
- run: |
- {
- echo "### Runner $RUNNER_NAME"
- echo
- for dev in /dev/kvm /dev/vhost-vsock; do
- if [ -r "$dev" ] && [ -w "$dev" ]; then echo "- $dev: usable"; else echo "- $dev: **not usable** ($(ls -l "$dev" 2>&1))"; fi
- done
- if sudo -n true 2>/dev/null; then
- echo "- sudo -n: yes"
- sudo -n modprobe nbd max_part=8 2>/dev/null || true
- for _ in $(seq 50); do [ -e /dev/nbd0 ] && break; sleep 0.02; done
- if [ -e /dev/nbd0 ]; then echo "- /dev/nbd0: present (image variants and the echo login run)"; else echo "- /dev/nbd0: **missing** (image variants are skipped, the login is agetty's)"; fi
- else
- echo "- sudo -n: **no** (image variants are skipped, the login is agetty's)"
- fi
- echo "- gh: $(command -v gh || echo '**missing**')"
- echo "- CPU: $(sed -n 's/^model name\s*: //p' /proc/cpuinfo | head -1), $(nproc) CPUs"
- } | tee -a "$GITHUB_STEP_SUMMARY"
- [ -r /dev/kvm ] && [ -w /dev/kvm ] || { echo "::error::no usable /dev/kvm: nothing here can be measured"; exit 1; }
-
# No Actions cache: this runner keeps Go's module and build caches on its own disk, and
# the cache service only stored another copy per branch, against the repository's limit.
- name: Set up Go
@@ -80,43 +60,23 @@ jobs:
go-version-file: go.mod
cache: false
- - name: Set up Task
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
+ # What this runner can do, said before anything is measured: a row the report skips is one
+ # this host lacks the device for, and the summary says which before the report does.
+ - name: What this runner has
+ run: go tool task report:runner
# The tarball as a consumer gets it, and not a tree a build left: what is measured is what
# was published.
- name: Unpack the release
- run: |
- set -euo pipefail
- rm -rf _output/report
- mkdir -p _output/report
- gh release download "$VERSION" -R "$GITHUB_REPOSITORY" -p '*.tar.gz' -D _output/report
- tar -xzf _output/report/*.tar.gz -C _output/report
- echo "RELEASE_TREE=$PWD/_output/report/spin-machine-${VERSION}-linux-x86_64/usr/share/spin-stack" >> "$GITHUB_ENV"
+ run: go tool task report:unpack
- name: Measure the feature matrix
- run: task report OUTPUT_DIR="$RELEASE_TREE" OUT="$PWD/_output/report/report.json"
+ run: go tool task report OUTPUT_DIR="$RELEASE_TREE" OUT="$PWD/_output/report/report.json"
# Against the newest release before this one that has a report. The first release with
# a report has nothing to compare with, and says so.
- name: Compare with the release before it
- run: |
- set -euo pipefail
- cd _output/report
- previous=""
- for tag in $(gh release list -R "$GITHUB_REPOSITORY" --limit 30 --json tagName -q '.[].tagName'); do
- [ "$tag" = "$VERSION" ] && continue
- if gh release download "$tag" -R "$GITHUB_REPOSITORY" -p report.json -O previous.json 2>/dev/null; then
- previous="$tag"
- break
- fi
- done
- if [ -n "$previous" ]; then
- "$RELEASE_TREE/bin/spin-machine" compare --old previous.json --new report.json > compare.md
- else
- echo "No earlier release published a report.json to compare the feature matrix with." > compare.md
- fi
- cat compare.md >> "$GITHUB_STEP_SUMMARY"
+ run: go tool task report:compare
- name: Keep the report
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
@@ -129,10 +89,4 @@ jobs:
- name: Publish the report, and what moved
if: inputs.publish
- run: |
- set -euo pipefail
- cd _output/report
- gh release upload "$VERSION" -R "$GITHUB_REPOSITORY" report.json --clobber
- gh release view "$VERSION" -R "$GITHUB_REPOSITORY" --json body -q .body > notes.md
- { echo; cat compare.md; } >> notes.md
- gh release edit "$VERSION" -R "$GITHUB_REPOSITORY" --notes-file notes.md
+ run: go tool task report:publish
diff --git a/README.md b/README.md
index f065b78..e02e8b8 100644
--- a/README.md
+++ b/README.md
@@ -121,8 +121,9 @@ task fingerprint # this machine's identity
task release # one tarball, one version
```
-`task` is [taskfile.dev](https://taskfile.dev), not GNU task. The part builds run in
-containers (`qemu/Dockerfile`, `kernel/Dockerfile`, `e2fsprogs/Dockerfile`,
+`task` is [taskfile.dev](https://taskfile.dev), not GNU task, at the version go.mod pins:
+`go tool task build` runs it with nothing installed, and is what CI runs. The part builds
+run in containers (`qemu/Dockerfile`, `kernel/Dockerfile`, `e2fsprogs/Dockerfile`,
`image/Dockerfile`), so a container engine is needed to build; a boot is not, and neither
is `task build` — a consumer points `--release` at an unpacked release tree instead.
A boot needs `/dev/kvm` readable and writable by whoever runs it, the same requirement
diff --git a/Taskfile.yml b/Taskfile.yml
index 7ae56b0..0c176fb 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -123,7 +123,7 @@ tasks:
default:
cmds:
- - task --list
+ - '{{.TASK_EXE}} --list'
# ==========================================================================
# Build
@@ -209,7 +209,7 @@ tasks:
- cmd: 'echo "OK: go vet"'
# `task --list` parses every included Taskfile, so a syntax error or a name defined
# twice fails here rather than in whichever lane first called it.
- - task --list > /dev/null
+ - '{{.TASK_EXE}} --list > /dev/null'
- cmd: 'echo "OK: the Taskfiles parse"'
- |
set -euo pipefail
@@ -219,12 +219,15 @@ tasks:
done
echo "OK: the shell scripts parse"
# go-tools' gates, at the version go.mod pins: every goroutine held to a context, no test
- # that cannot fail, every path and task AGENTS.md and CLAUDE.md name resolves, and every
- # mutate-exempt reason covers an edit mutate makes.
+ # that cannot fail, every path and task AGENTS.md and CLAUDE.md name resolves, every
+ # mutate-exempt reason covers an edit mutate makes, every workflow step is one task, and
+ # pnpm is the one package manager, installing through Socket Firewall.
- go tool ctxlife
- go tool testquality ./...
- go tool refs
- go tool mutate -stale-exempts
+ - go tool taskgate
+ - go tool sfw check
- task: lint:workflows
lint:workflows:
@@ -246,11 +249,70 @@ tasks:
mutate:
desc: >-
Break what the change against BASE touched and fail on an edit no test refuses
- (task mutate BASE=origin/main).
- vars:
- BASE: '{{.BASE | default "origin/main"}}'
+ (task mutate BASE=origin/main, the default). In CI the base is what the change is to: a
+ pull request's target branch, or on a push what the push replaced (BEFORE).
+ cmds:
+ - |
+ set -euo pipefail
+ base='{{.BASE}}'
+ if [ -z "$base" ] && [ "${GITHUB_ACTIONS:-}" = true ]; then
+ if [ "$GITHUB_EVENT_NAME" = pull_request ]; then
+ base="origin/$GITHUB_BASE_REF"
+ else
+ base="${BEFORE:-}"
+ fi
+ # A push that created its branch has no commit before it.
+ git rev-parse --verify "$base^{commit}" >/dev/null 2>&1 || base=HEAD~1
+ fi
+ go tool mutate -base "${base:-origin/main}" -max 2000
+
+ coverage:
+ desc: >-
+ What `task test` covered, function by function: on the run's summary in CI, on stdout
+ here, and as coverage.html.
+ cmds:
+ - |
+ set -e
+ summary=${GITHUB_STEP_SUMMARY:-/dev/stdout}
+ if [ ! -f coverage.out ]; then
+ echo "coverage.out was not produced; the tests may have failed before coverage ran." >> "$summary"
+ exit 0
+ fi
+ {
+ echo "## Go coverage"
+ echo
+ echo "**$(go tool cover -func=coverage.out | awk '/^total:/ {print $3}')** total"
+ echo
+ echo 'Function detail
'
+ echo
+ echo '```'
+ go tool cover -func=coverage.out
+ echo '```'
+ echo ' '
+ } >> "$summary"
+ go tool cover -html=coverage.out -o coverage.html
+
+ touches:
+ desc: >-
+ Whether the commits since BASE can change what one part builds, as build=true or
+ build=false, on the step's outputs in CI (task touches BASE= -- ... --
+ ...; hack/touches says how it decides).
+ cmds:
+ - |
+ set -e
+ build=$(hack/touches "${BASE:-}" {{.CLI_ARGS}})
+ echo "build=${build}" | tee -a "${GITHUB_OUTPUT:-/dev/null}"
+
+ vsock:
+ desc: >-
+ Make /dev/vhost-vsock usable by this user, so verify:args can create the machine's vsock
+ device. Without it, says so rather than failing: verify:args names the device unchecked.
cmds:
- - go tool mutate -base {{.BASE}} -max 2000
+ # The device first: a kernel that builds it in - Spin OS's, which has no modules - has it
+ # from boot, and modprobe there has nothing to find.
+ - |
+ { [ -c /dev/vhost-vsock ] || sudo modprobe vhost_vsock; } && sudo chmod 0666 /dev/vhost-vsock ||
+ echo "no vhost_vsock here; the check will name vhost-vsock-pci as unchecked"
verify:args:
desc: >-
@@ -316,6 +378,14 @@ tasks:
"$dir/osv.json" | sort -u > _output/osv.tsv
echo "$tag: $(wc -l < _output/osv.tsv) finding(s), in _output/osv.tsv"
+ osv:issues:
+ desc: >-
+ Bring the issues to what task osv found: one open for each package with findings, closed
+ once the newest release has none (hack/osvissues says why a package and not a finding).
+ Needs gh.
+ cmds:
+ - go run ./hack/osvissues -release "$(gh release view --json tagName -q .tagName)" -findings _output/osv.tsv
+
release:
desc: Build everything and pack one versioned tarball.
cmds:
@@ -325,6 +395,137 @@ tasks:
# that could disagree with what was built.
- OUTPUT_DIR="{{.OUTPUT_ABS}}" hack/release
+ # What release.yml does around `task release`, in its order. The version and the identity a file
+ # is signed as are the workflow's to know, and come in its environment.
+
+ release:version:
+ desc: >-
+ The version a release is: INPUT_VERSION, or a new one for today, counted from the tags
+ today already has. On the step's outputs in CI.
+ cmds:
+ - |
+ set -euo pipefail
+ if [ -n "${INPUT_VERSION:-}" ]; then
+ version="$INPUT_VERSION"
+ origin="given by the caller"
+ else
+ day=$(date -u +%Y%m%d)
+ # Count what today already has. `git tag -l` on a fetch-depth:0 checkout sees
+ # every tag; the sequence is two digits so that v20260909.02 sorts after .01
+ # and both sort before v20260910.01, as strings, everywhere.
+ taken=$(git tag -l "v${day}.*" | wc -l)
+ version=$(printf 'v%s.%02d' "$day" "$((taken + 1))")
+ origin="generated for today (${taken} already taken)"
+ fi
+
+ case "$version" in
+ v[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].[0-9][0-9]) ;;
+ *) echo "::error::'$version' is not vYYYYMMDD.NN" >&2; exit 1 ;;
+ esac
+
+ echo "version=$version" >> "${GITHUB_OUTPUT:-/dev/stdout}"
+ echo "Releasing $version — $origin"
+
+ release:notes:
+ desc: >-
+ The notes of the release VERSION built in _output/release: what changed since the release
+ before it, whether its machine is that one's (hack/fingerprint-diff), and its machine.env.
+ On the step's outputs in CI. Needs gh.
+ cmds:
+ - |
+ set -euo pipefail
+ name="spin-machine-${VERSION}-linux-x86_64"
+ manifest="_output/release/${name}/usr/share/spin-stack/machine.env"
+ cat "$manifest"
+
+ # The release before this one, for both the verdict and the notes: the newest published
+ # one that is not this version, so a re-run of a version that already published compares
+ # against its predecessor rather than against itself. A listing that fails fails the
+ # step - read as "no releases", it would announce a first release that is not one.
+ releases=$(gh release list -R "$GITHUB_REPOSITORY" --limit 30 --json tagName -q '.[].tagName')
+ previous=$(printf '%s\n' "$releases" | grep -vFx -- "$VERSION" | head -1 || true)
+
+ if [ -n "$previous" ]; then
+ # machine.env's lines are the script's key=value arguments, as they are.
+ # shellcheck disable=SC2046
+ REFERENCE="$previous" hack/fingerprint-diff $(grep '_sha256=' "$manifest") > /tmp/verdict.md
+ # What reached the machine since: the commits by the part they changed, the pins
+ # that moved, the kernel options and the patches, read from git.
+ go run ./hack/releasenotes -from "$previous" -to HEAD > /tmp/changes.md
+ else
+ echo "Nothing to compare against: this is the first published release." > /tmp/verdict.md
+ echo "The first release: nothing before it to say what changed against." > /tmp/changes.md
+ fi
+ # Whether the QEMU shipped offers a machine type newer than the one every checkpoint is
+ # saved on: said, never acted on (machine.MachineType).
+ offer=$(go run ./hack/machinetype check -qemu "_output/release/${name}/usr/share/spin-stack/bin/qemu-system-x86_64")
+ if [ -n "$offer" ]; then printf '\n%s\n' "$offer" >> /tmp/verdict.md; fi
+ cat /tmp/verdict.md
+ cat /tmp/changes.md
+
+ {
+ echo 'notes<> "${GITHUB_OUTPUT:-/dev/stdout}"
+
+ release:provenance:
+ desc: >-
+ Who built the release VERSION and from what - this commit, every pin in versions.yaml - as
+ SLSA provenance, in _output/provenance.json.
+ cmds:
+ - go tool provenance -version "${VERSION}" -out _output/provenance.json
+
+ release:sign:
+ desc: >-
+ Sign and attest every file of _output/release as IDENTITY, the workflow at its ref, and
+ check each bundle against it. Needs cosign and an OIDC token, so it is release.yml's.
+ # An attestation is checked by the file's digest, not the file: verify-blob-attestation reads
+ # a blob whole and refuses one over COSIGN_MAX_ATTACHMENT_SIZE (128 MiB), and the tarball is
+ # 338 MB (v20261013.01's run failed there). The digest is what the in-toto subject names.
+ cmds:
+ - |
+ set -euo pipefail
+ attested() {
+ cosign verify-blob-attestation --type "$1" --bundle "$2" \
+ --digest "$(sha256sum "$3" | cut -d' ' -f1)" --digestAlg sha256 \
+ --certificate-identity "${IDENTITY}" \
+ --certificate-oidc-issuer https://token.actions.githubusercontent.com
+ }
+ for f in _output/release/*.tar.gz _output/release/*.tar.gz.sha256 _output/release/machine.env _output/release/*.cdx.json; do
+ cosign sign-blob --yes --bundle "${f}.sigstore.json" "${f}" > /dev/null
+ cosign attest-blob --yes --type slsaprovenance1 --predicate _output/provenance.json \
+ --bundle "${f}.provenance.sigstore.json" "${f}" > /dev/null
+ cosign verify-blob --bundle "${f}.sigstore.json" --certificate-identity "${IDENTITY}" \
+ --certificate-oidc-issuer https://token.actions.githubusercontent.com "${f}"
+ attested slsaprovenance1 "${f}.provenance.sigstore.json" "${f}"
+ done
+ # The tarball's SBOM (hack/release), attested over it: what it is made of, by the
+ # workflow that built it. The same SBOM is inside it, at usr/share/spin-stack/sbom.
+ for f in _output/release/*.tar.gz; do
+ cosign attest-blob --yes --type cyclonedx --predicate "${f%.tar.gz}.cdx.json" \
+ --bundle "${f}.cdx.sigstore.json" "${f}" > /dev/null
+ attested cyclonedx "${f}.cdx.sigstore.json" "${f}"
+ done
+ go tool attestgate files -require provenance _output/release
+ go tool attestgate files -require cdx -except '*.sha256,machine.env,*.cdx.json' _output/release
+
+ release:tag:
+ desc: Tag this commit VERSION and push the tag. A version tagged already is refused, since git push does not move a tag.
+ cmds:
+ - |
+ set -euo pipefail
+ git tag "$VERSION"
+ git push origin "$VERSION"
+
# ==========================================================================
# Trying the machine
# ==========================================================================
@@ -426,6 +627,223 @@ tasks:
SPIN_REPORT_ONLY='{{.ONLY}}' SPIN_REPORT_FLAGS='{{.FLAGS}}' REPS={{.REPS}}
go test ./boot/ -run '^TestReport$' -count=1 -v -timeout 6h
+ # ==========================================================================
+ # The KVM runner: what lab.yml and report.yml do around a target
+ # ==========================================================================
+ #
+ # In their order. What the dispatch asked for comes in the job's environment (RELEASE,
+ # EXPERIMENT, KERNEL_RUN, KERNEL_RUN_C, QEMU_RUN, VARIANT, VARS; VERSION for a report), and what
+ # one step finds reaches the next through $GITHUB_ENV.
+
+ lab:unpack:
+ desc: The release RELEASE, or the newest, unpacked into _output/lab as the tree the experiment boots. Needs gh.
+ cmds:
+ - |
+ set -euo pipefail
+ if [ -z "$RELEASE" ]; then
+ RELEASE=$(gh release list -R "$GITHUB_REPOSITORY" --exclude-drafts --exclude-pre-releases --limit 1 --json tagName -q '.[0].tagName')
+ fi
+ rm -rf _output/lab
+ mkdir -p _output/lab/tmp
+ gh release download "$RELEASE" -R "$GITHUB_REPOSITORY" -p '*.tar.gz' -D _output/lab
+ tar -xzf _output/lab/*.tar.gz -C _output/lab
+ {
+ echo "RELEASE=$RELEASE"
+ echo "SPIN_MACHINE_OUTPUT=$PWD/_output/lab/spin-machine-${RELEASE}-linux-x86_64/usr/share/spin-stack"
+ # On disk: the overlays and memory files go here, gigabytes a tmpfs /tmp holds in RAM.
+ echo "TMPDIR=$PWD/_output/lab/tmp"
+ } >> "${GITHUB_ENV:-/dev/stdout}"
+
+ lab:fetch:
+ desc: The builds to compare, from the runs KERNEL_RUN, KERNEL_RUN_C and QEMU_RUN name, into _output/lab. Needs gh.
+ cmds:
+ - |
+ set -euo pipefail
+ fetch() { # run, artifact pattern, directory
+ [[ "$1" =~ ^[0-9]+$ ]] || { echo "::error::a run id is a number, got: $1"; exit 1; }
+ gh run download "$1" -R "$GITHUB_REPOSITORY" -p "$2" -D "$3"
+ }
+ if [ -n "$KERNEL_RUN" ]; then fetch "$KERNEL_RUN" 'vmlinux-*' _output/lab/kernel-b; fi
+ if [ -n "$KERNEL_RUN_C" ]; then fetch "$KERNEL_RUN_C" 'vmlinux-*' _output/lab/kernel-c; fi
+ if [ -n "$QEMU_RUN" ]; then fetch "$QEMU_RUN" 'qemu-*-linux-amd64' _output/lab/qemu-b; fi
+
+ lab:place:
+ desc: >-
+ Put the fetched builds where the experiment boots them: beside the release's (VARIANT=beside)
+ or in place of it (VARIANT=instead).
+ cmds:
+ - |
+ set -euo pipefail
+ env=${GITHUB_ENV:-/dev/stdout}
+ one() { # the one file under $1 whose path matches $2
+ f=$(find "$1" -path "$2" -type f | head -1)
+ [ -n "$f" ] || { echo "::error::$1 has no $2"; exit 1; }
+ echo "$PWD/$f"
+ }
+ b=$PWD/_output/lab/release-b
+ tree_b() { [ -d "$b" ] || cp -as "$SPIN_MACHINE_OUTPUT" "$b"; }
+ if [ -n "$KERNEL_RUN" ]; then
+ k=$(one _output/lab/kernel-b '*/vmlinux')
+ if [ "$VARIANT" = instead ]; then
+ tree_b; rm -f "$b/kernel/vmlinux"; install -m 0644 "$k" "$b/kernel/vmlinux"
+ else
+ echo "SPIN_KERNEL_B=$k" >> "$env"
+ fi
+ fi
+ if [ -n "$KERNEL_RUN_C" ]; then
+ [ "$VARIANT" = beside ] || { echo "::error::kernel_run_c is a third kernel beside the release, not one to boot instead"; exit 1; }
+ echo "SPIN_KERNEL_C=$(one _output/lab/kernel-c '*/vmlinux')" >> "$env"
+ fi
+ if [ -n "$QEMU_RUN" ]; then
+ q=$(one _output/lab/qemu-b '*/bin/qemu-system-x86_64')
+ tree_b; rm -rf "$b/bin/qemu-system-x86_64" "$b/qemu"
+ install -m 0755 "$q" "$b/bin/qemu-system-x86_64"
+ cp -r "$(dirname "$(dirname "$q")")/qemu" "$b/qemu"
+ [ "$VARIANT" = instead ] || echo "SPIN_MACHINE_OUTPUT_B=$b" >> "$env"
+ fi
+ if [ "$VARIANT" = instead ]; then echo "SPIN_MACHINE_OUTPUT=$b" >> "$env"; fi
+
+ lab:vars:
+ desc: >-
+ VARS, `KEY=VALUE; KEY=VALUE`, for the experiment: only the names a target reads, so a typo
+ fails here rather than being ignored, and none of the ones lab:unpack and lab:place set.
+ cmds:
+ - |
+ set -euo pipefail
+ IFS=';' read -ra pairs <<< "$VARS"
+ for pair in "${pairs[@]}"; do
+ pair="${pair#"${pair%%[![:space:]]*}"}"
+ pair="${pair%"${pair##*[![:space:]]}"}"
+ [ -z "$pair" ] && continue
+ key="${pair%%=*}"
+ [[ "$pair" == *=* && "$key" =~ ^(REPS|TOP|GAP|ONLY|FLAGS|CPUS|MEMORY_MIB|DEBUG|FILE_MB|CACHE_TIMEOUT_S|GRAPH_[A-Z_]+|SHARE_[A-Z_]+|SPIN_[A-Z0-9_]+)$ ]] ||
+ { echo "::error::not a variable this workflow passes on: $pair"; exit 1; }
+ case "$key" in
+ SPIN_MACHINE_OUTPUT|SPIN_MACHINE_OUTPUT_B|SPIN_KERNEL_B|SPIN_KERNEL_C) echo "::error::$key is set by this workflow"; exit 1 ;;
+ esac
+ echo "$pair" >> "${GITHUB_ENV:-/dev/stdout}"
+ done
+
+ lab:about:
+ desc: What this lab run is and the host it runs on, on the run's summary in CI; fails without a usable /dev/kvm.
+ cmds:
+ - |
+ set -e
+ {
+ echo "### $EXPERIMENT on ${RUNNER_NAME:-$(hostname)}"
+ echo
+ echo "- release: $RELEASE"
+ echo "- kernel B: ${KERNEL_RUN:+run }${KERNEL_RUN:-none}"
+ echo "- kernel C: ${KERNEL_RUN_C:+run }${KERNEL_RUN_C:-none}"
+ echo "- QEMU B: ${QEMU_RUN:+run }${QEMU_RUN:-none}"
+ if [ -n "$KERNEL_RUN$KERNEL_RUN_C$QEMU_RUN" ]; then echo "- booted: $VARIANT the release"; fi
+ echo "- vars: ${VARS:-none}"
+ echo "- CPU: $(sed -n 's/^model name\s*: //p' /proc/cpuinfo | head -1), $(nproc) CPUs"
+ echo "- load: $(cut -d' ' -f1-3 /proc/loadavg)"
+ if sudo -n true 2>/dev/null; then echo "- sudo -n: yes"; else echo "- sudo -n: **no** (targets that edit an overlay skip)"; fi
+ } | tee -a "${GITHUB_STEP_SUMMARY:-/dev/null}"
+ [ -r /dev/kvm ] && [ -w /dev/kvm ] || { echo "::error::no usable /dev/kvm"; exit 1; }
+
+ lab:run:
+ desc: >-
+ The experiment EXPERIMENT, on the tree SPIN_MACHINE_OUTPUT, logged to _output/lab/lab.log;
+ report writes its JSON to _output/lab/report.json.
+ # A skipped test exits 0, and a green run that measured nothing reads as a result, so a skip
+ # fails the run with the test's reason.
+ cmds:
+ - |
+ set -o pipefail
+ {{.TASK_EXE}} "$EXPERIMENT" OUTPUT_DIR="$SPIN_MACHINE_OUTPUT" OUT="$PWD/_output/lab/report.json" 2>&1 | tee _output/lab/lab.log
+ if grep -q -- '--- SKIP' _output/lab/lab.log; then
+ echo "::error::the experiment skipped: $(grep -B1 -- '--- SKIP' _output/lab/lab.log | head -1 | sed 's/^.*_test.go:[0-9]*: //')"
+ exit 1
+ fi
+
+ lab:found:
+ desc: What the experiment printed, from _output/lab/lab.log, on the run's summary in CI.
+ cmds:
+ - |
+ set -e
+ [ -f _output/lab/lab.log ] || exit 0
+ {
+ echo
+ echo '```'
+ grep -E '^\s{4}\S' _output/lab/lab.log | sed 's/^ //' | tail -300
+ echo '```'
+ } >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}"
+
+ report:runner:
+ desc: What this host can measure, said before anything is, on the run's summary in CI; fails without a usable /dev/kvm.
+ # A row the report skips is one this host lacks the device for, and this says which first.
+ cmds:
+ - |
+ set -e
+ {
+ echo "### Runner ${RUNNER_NAME:-$(hostname)}"
+ echo
+ for dev in /dev/kvm /dev/vhost-vsock; do
+ if [ -r "$dev" ] && [ -w "$dev" ]; then echo "- $dev: usable"; else echo "- $dev: **not usable** ($(ls -l "$dev" 2>&1))"; fi
+ done
+ if sudo -n true 2>/dev/null; then
+ echo "- sudo -n: yes"
+ sudo -n modprobe nbd max_part=8 2>/dev/null || true
+ for _ in $(seq 50); do [ -e /dev/nbd0 ] && break; sleep 0.02; done
+ if [ -e /dev/nbd0 ]; then echo "- /dev/nbd0: present (image variants and the echo login run)"; else echo "- /dev/nbd0: **missing** (image variants are skipped, the login is agetty's)"; fi
+ else
+ echo "- sudo -n: **no** (image variants are skipped, the login is agetty's)"
+ fi
+ echo "- gh: $(command -v gh || echo '**missing**')"
+ echo "- CPU: $(sed -n 's/^model name\s*: //p' /proc/cpuinfo | head -1), $(nproc) CPUs"
+ } | tee -a "${GITHUB_STEP_SUMMARY:-/dev/null}"
+ [ -r /dev/kvm ] && [ -w /dev/kvm ] || { echo "::error::no usable /dev/kvm: nothing here can be measured"; exit 1; }
+
+ report:unpack:
+ desc: >-
+ The release VERSION's tarball, as a consumer gets it, unpacked into _output/report: what is
+ measured is what was published. Needs gh.
+ cmds:
+ - |
+ set -euo pipefail
+ rm -rf _output/report
+ mkdir -p _output/report
+ gh release download "$VERSION" -R "$GITHUB_REPOSITORY" -p '*.tar.gz' -D _output/report
+ tar -xzf _output/report/*.tar.gz -C _output/report
+ echo "RELEASE_TREE=$PWD/_output/report/spin-machine-${VERSION}-linux-x86_64/usr/share/spin-stack" >> "${GITHUB_ENV:-/dev/stdout}"
+
+ report:compare:
+ desc: >-
+ _output/report/report.json against the newest release before VERSION that published one,
+ in compare.md beside it and on the run's summary in CI. Needs gh.
+ cmds:
+ - |
+ set -euo pipefail
+ cd _output/report
+ previous=""
+ for tag in $(gh release list -R "$GITHUB_REPOSITORY" --limit 30 --json tagName -q '.[].tagName'); do
+ [ "$tag" = "$VERSION" ] && continue
+ if gh release download "$tag" -R "$GITHUB_REPOSITORY" -p report.json -O previous.json 2>/dev/null; then
+ previous="$tag"
+ break
+ fi
+ done
+ if [ -n "$previous" ]; then
+ "$RELEASE_TREE/bin/spin-machine" compare --old previous.json --new report.json > compare.md
+ else
+ echo "No earlier release published a report.json to compare the feature matrix with." > compare.md
+ fi
+ cat compare.md >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}"
+
+ report:publish:
+ desc: Upload _output/report/report.json to the release VERSION and append compare.md to its notes. Needs gh.
+ cmds:
+ - |
+ set -euo pipefail
+ cd _output/report
+ gh release upload "$VERSION" -R "$GITHUB_REPOSITORY" report.json --clobber
+ gh release view "$VERSION" -R "$GITHUB_REPOSITORY" --json body -q .body > notes.md
+ { echo; cat compare.md; } >> notes.md
+ gh release edit "$VERSION" -R "$GITHUB_REPOSITORY" --notes-file notes.md
+
# ==========================================================================
# Keeping the pins and the tree
# ==========================================================================
@@ -448,7 +866,7 @@ tasks:
- '{{.VERSIONS}} bump {{.NAME}} {{.TO}}'
# A QEMU bump keeps the machine type (machine.MachineType), and the bump's pull request
# shows what the newer types change against it, from the source just pinned.
- - cmd: task qemu:compat
+ - cmd: '{{.TASK_EXE}} qemu:compat'
if: '[ "{{.NAME}}" = qemu ]'
clean:
diff --git a/boot/Taskfile.yml b/boot/Taskfile.yml
index 810f172..378a249 100644
--- a/boot/Taskfile.yml
+++ b/boot/Taskfile.yml
@@ -52,6 +52,37 @@ tasks:
fi
echo "$dir/$name/usr/share/spin-stack" > {{.OUTPUT_ABS}}/compat/previous
+ buildkit:
+ desc: >-
+ Use the BuildKit builder named compat, made the first time: a docker-container builder,
+ which keeps its cache in .cache/buildkit for the KVM runner's next run.
+ cmds:
+ - docker buildx inspect compat > /dev/null 2>&1 && docker buildx use compat || docker buildx create --use --name compat --driver docker-container
+
+ here:
+ desc: >-
+ The machine before (boot:previous) and this one: the newest release with this tree's QEMU,
+ kernel and spin-machine in place of its own, in _output/compat/here. Both are exported to
+ the steps after in CI (SPIN_COMPAT_PREVIOUS, SPIN_MACHINE_OUTPUT). Needs a built QEMU,
+ kernel and tools.
+ # The base image is the release's: a checkpoint resumes with the disks it was saved with.
+ cmds:
+ - task: previous
+ - |
+ set -euo pipefail
+ previous=$(cat {{.OUTPUT_ABS}}/compat/previous)
+ here={{.OUTPUT_ABS}}/compat/here
+ rm -rf "$here" && cp -as "$previous" "$here"
+ rm -rf "$here/bin" "$here/qemu" "$here/kernel"
+ mkdir -p "$here/bin" "$here/kernel"
+ cp {{.OUTPUT_ABS}}/bin/qemu-system-x86_64 {{.OUTPUT_ABS}}/bin/qemu-img {{.OUTPUT_ABS}}/bin/spin-machine "$here/bin/"
+ cp -r {{.OUTPUT_ABS}}/qemu "$here/qemu"
+ cp {{.OUTPUT_ABS}}/kernel/vmlinux "$here/kernel/vmlinux"
+ {
+ echo "SPIN_COMPAT_PREVIOUS=$previous"
+ echo "SPIN_MACHINE_OUTPUT=$here"
+ } >> "${GITHUB_ENV:-/dev/stdout}"
+
compat:
desc: >-
A checkpoint the newest published release saves resumes on this tree's build, when the two
diff --git a/go.mod b/go.mod
index d5c6181..ec93e61 100644
--- a/go.mod
+++ b/go.mod
@@ -5,25 +5,154 @@ go 1.27
toolchain go1.27.3
tool (
+ github.com/go-task/task/v3/cmd/task
github.com/spin-stack/go-tools/cmd/attestgate
github.com/spin-stack/go-tools/cmd/ctxlife
github.com/spin-stack/go-tools/cmd/mutate
github.com/spin-stack/go-tools/cmd/provenance
github.com/spin-stack/go-tools/cmd/refs
github.com/spin-stack/go-tools/cmd/sbom
+ github.com/spin-stack/go-tools/cmd/sfw
+ github.com/spin-stack/go-tools/cmd/taskgate
github.com/spin-stack/go-tools/cmd/testquality
github.com/spin-stack/go-tools/cmd/versions
)
require (
- github.com/spin-stack/go-tools v0.0.0-20261013164036-99d744796066
+ github.com/spin-stack/go-tools v0.0.0-20261024191920-5824d9a153be
golang.org/x/sys v0.49.0
)
require (
+ cel.dev/expr v0.25.2 // indirect
+ charm.land/bubbles/v2 v2.1.1 // indirect
+ charm.land/bubbletea/v2 v2.0.10 // indirect
+ charm.land/lipgloss/v2 v2.0.6 // indirect
+ cloud.google.com/go v0.123.0 // indirect
+ cloud.google.com/go/auth v0.24.0 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.3.0 // indirect
+ cloud.google.com/go/compute/metadata v0.10.0 // indirect
+ cloud.google.com/go/iam v1.14.0 // indirect
+ cloud.google.com/go/monitoring v1.31.0 // indirect
+ cloud.google.com/go/storage v1.65.1 // indirect
github.com/CycloneDX/cyclonedx-go v0.12.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.38.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.62.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.62.0 // indirect
+ github.com/Ladicle/tabwriter v1.0.0 // indirect
+ github.com/Masterminds/semver/v3 v3.5.0 // indirect
+ github.com/alecthomas/chroma/v2 v2.27.0 // indirect
+ github.com/atotto/clipboard v0.1.4 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.44.0 // indirect
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.32.40 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.19.39 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.10.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.41 // indirect
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.108.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/signin v1.6.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.34.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.46.0 // indirect
+ github.com/aws/smithy-go v1.28.1 // indirect
+ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/chainguard-dev/git-urls v1.0.2 // indirect
+ github.com/charmbracelet/colorprofile v0.4.3 // indirect
+ github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect
+ github.com/charmbracelet/x/ansi v0.11.8 // indirect
+ github.com/charmbracelet/x/term v0.2.2 // indirect
+ github.com/charmbracelet/x/termios v0.1.1 // indirect
+ github.com/charmbracelet/x/windows v0.2.2 // indirect
+ github.com/clipperhouse/displaywidth v0.11.0 // indirect
+ github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
+ github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/dlclark/regexp2/v2 v2.7.1 // indirect
+ github.com/docker/cli v29.7.2+incompatible // indirect
+ github.com/docker/docker-credential-helpers v0.9.3 // indirect
+ github.com/dominikbraun/graph v0.23.0 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/elliotchance/orderedmap/v3 v3.1.1 // indirect
+ github.com/envoyproxy/go-control-plane/envoy v1.39.0 // indirect
+ github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
+ github.com/fatih/color v1.19.0 // indirect
+ github.com/felixge/httpsnoop v1.1.0 // indirect
+ github.com/fsnotify/fsnotify v1.10.1 // indirect
+ github.com/go-jose/go-jose/v4 v4.1.4 // indirect
+ github.com/go-logr/logr v1.4.4 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
+ github.com/go-task/task/v3 v3.54.0 // indirect
+ github.com/go-task/template v0.2.0 // indirect
+ github.com/google/go-containerregistry v0.22.1 // indirect
+ github.com/google/s2a-go v0.1.10 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.22 // indirect
+ github.com/googleapis/gax-go/v2 v2.26.2 // indirect
+ github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 // indirect
+ github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+ github.com/hashicorp/go-getter v1.8.9 // indirect
+ github.com/hashicorp/go-version v1.9.0 // indirect
+ github.com/joho/godotenv v1.5.1 // indirect
+ github.com/klauspost/compress v1.19.2 // indirect
+ github.com/klauspost/cpuid/v2 v2.4.0 // indirect
+ github.com/klauspost/pgzip v1.2.6 // indirect
+ github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
+ github.com/mattn/go-colorable v0.1.15 // indirect
+ github.com/mattn/go-isatty v0.0.24 // indirect
+ github.com/mattn/go-runewidth v0.0.27 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
+ github.com/pierrec/lz4/v4 v4.1.27 // indirect
+ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
+ github.com/puzpuzpuz/xsync/v4 v4.5.0 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/sajari/fuzzy v1.0.0 // indirect
+ github.com/sirupsen/logrus v1.9.4 // indirect
+ github.com/spf13/pflag v1.0.10 // indirect
+ github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect
+ github.com/stretchr/objx v0.5.3 // indirect
+ github.com/stretchr/testify v1.12.1 // indirect
+ github.com/u-root/u-root v0.16.0 // indirect
+ github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
+ github.com/ulikunitz/xz v0.5.16 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ github.com/zeebo/xxh3 v1.1.0 // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+ go.opentelemetry.io/contrib/detectors/gcp v1.45.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect
+ go.opentelemetry.io/otel v1.45.0 // indirect
+ go.opentelemetry.io/otel/metric v1.45.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.45.0 // indirect
+ go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
+ go.opentelemetry.io/otel/trace v1.45.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
- golang.org/x/mod v0.41.0 // indirect
- golang.org/x/sync v0.23.0 // indirect
- golang.org/x/tools v0.51.0 // indirect
+ golang.org/x/crypto v0.58.0 // indirect
+ golang.org/x/mod v0.42.0 // indirect
+ golang.org/x/net v0.61.0 // indirect
+ golang.org/x/oauth2 v0.37.0 // indirect
+ golang.org/x/sync v0.24.0 // indirect
+ golang.org/x/term v0.47.0 // indirect
+ golang.org/x/text v0.43.0 // indirect
+ golang.org/x/time v0.16.0 // indirect
+ golang.org/x/tools v0.52.0 // indirect
+ google.golang.org/api v0.299.0 // indirect
+ google.golang.org/genproto v0.0.0-20260724162435-b2f20204f0df // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459 // indirect
+ google.golang.org/grpc v1.84.0 // indirect
+ google.golang.org/protobuf v1.36.12 // indirect
+ mvdan.cc/sh/v3 v3.14.1 // indirect
+ mvdan.cc/sh/x v0.0.0-20260922090537-aebdf2b86f56 // indirect
)
diff --git a/go.sum b/go.sum
index 70e4f9b..e6064d2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,36 +1,335 @@
+cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
+cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
+charm.land/bubbles/v2 v2.1.1 h1:7r55WzBxpo/R3z98hGmY7KKPd3ET6vsf0Fb9sDHOV60=
+charm.land/bubbles/v2 v2.1.1/go.mod h1:GE6M31gaWZVXzGw73OeuTTgy4lX+OtkH0E5ymnNsHxo=
+charm.land/bubbletea/v2 v2.0.10 h1:oolvo20VBpI0PfqE7iFjkZ1bx0WpmXGfnKz5Yldjq5o=
+charm.land/bubbletea/v2 v2.0.10/go.mod h1:QOatcnhOjYIfxzUSTz6raF7Ex4R/rIuHa3SnBdCCpMc=
+charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
+charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
+cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
+cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
+cloud.google.com/go/auth v0.24.0 h1:UYMbF8otPZnLAkNJ5/LYQYOq0ARcJS1P4JqTeMKbCYU=
+cloud.google.com/go/auth v0.24.0/go.mod h1:IFG/AMA1VWfuTrdbieEsB2GcpJyJV/phGAvogkOoPR4=
+cloud.google.com/go/auth/oauth2adapt v0.3.0 h1:FY8oSZpCYoUNv6QxVODuMjQz4IlSOVeiQtZ08vLPz88=
+cloud.google.com/go/auth/oauth2adapt v0.3.0/go.mod h1:7+2uCm7++XFO+/lN06c2HXpDXb/NMNn2/UwyBPbTnkk=
+cloud.google.com/go/compute/metadata v0.10.0 h1:pyKMUQSwchgkIBBJGdILqQbs/BNJXqwSA7Ej6LAvvtY=
+cloud.google.com/go/compute/metadata v0.10.0/go.mod h1:rGFHRrIif570kSibjFTMbt6/4/tzgJWFGI/HVol4GIk=
+cloud.google.com/go/iam v1.14.0 h1:YRIB1/4xxwJ+NfI6RQr1WaOoHHeV0MizJEj940nBXJA=
+cloud.google.com/go/iam v1.14.0/go.mod h1:JTPZfbHdGXJrOYREtkRoFzCDGaBdbA7ILLKO/DDuav0=
+cloud.google.com/go/logging v1.19.0 h1:NCqhdVUg3wQ8Cobdf16FDSuTGi3+6+hdSBHrY5TsR6Q=
+cloud.google.com/go/logging v1.19.0/go.mod h1:i40NZCHC9Gqvod4yE+yQfDWwlgwW/SrshkkGibCHxcA=
+cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM=
+cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0=
+cloud.google.com/go/monitoring v1.31.0 h1:LCuAoYiq8snyL/eDAG/VC73uWFXeax8+YEfmoNGJUrY=
+cloud.google.com/go/monitoring v1.31.0/go.mod h1:7uk2dIJIf7ra64dI1VuQ0ttoFqvtKjdzq46u6zNP1hU=
+cloud.google.com/go/storage v1.65.1 h1:LRRpBJUTf+OXDPX9jZUKZ3mSLIsz3htG+qUpeNZovyA=
+cloud.google.com/go/storage v1.65.1/go.mod h1:UsS9OgFg/XHOSYakQ8ZtLWWeyGkk1WnmD/GsGfN0BHM=
+cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E=
+cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0=
github.com/CycloneDX/cyclonedx-go v0.12.0 h1:/7Jum36UA6V043tQZ/fE3jf+Nf9gn/qxUFfd7QReMy8=
github.com/CycloneDX/cyclonedx-go v0.12.0/go.mod h1:V2577HhxDDCDLYfkm55WJrz16nHTfyQZwcWUBSG7Z28=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.38.0 h1:rmzbFS2CkHq0y2L5LkMVWm+QXjjNEXxjmhrJqga/QGk=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.38.0/go.mod h1:iIyrdhveU5Ow84ipRlxtjCOQnIM0DiqSMxuxm50xwZY=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.62.0 h1:5y2LR2pripbccOte6BpZ+guv5O5W0OQIfKvUYZrGaAQ=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.62.0/go.mod h1:b3f7EifFRqkVt142USoGIP62uUeZm7ALCPrs1ZjWXYo=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.62.0 h1:tRsXhJnn0w0TNwJpVrB94wsrimzjWKFzdQlUhRdNUzc=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.62.0/go.mod h1:ifhe5teRNcbxg9p353DhOkOgczzL/IBLYFqT1cm1zpM=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.62.0 h1:aPMa5qZU9dp4fU34o+jQgVIPFIOi20oxYg8TLigmZLg=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.62.0/go.mod h1:sg6Wqbl0BKKZ5zKnQj2q0qic/3ziF894SCx1LpbX37Y=
+github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
+github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
+github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
+github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
+github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
+github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
+github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aws/aws-sdk-go-v2 v1.44.0 h1:4IbaHhtzy+4h37z4JQyO9a2QsiCml3CNYHtq5hIHigo=
+github.com/aws/aws-sdk-go-v2 v1.44.0/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20/go.mod h1:g7PNzKcsOKWb4fkSRBA7BZVAS6Y8IcxzN+nRohhQ1Q8=
+github.com/aws/aws-sdk-go-v2/config v1.32.40 h1:lAVC9gMmKusmqDRe32dPtgKl/BWvJmMJoWELKHCAObw=
+github.com/aws/aws-sdk-go-v2/config v1.32.40/go.mod h1:8xOJLbe/hOj1g4PVsfJYV7O2byq+UGET1onDdUgbwqc=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.39 h1:XOg8LC3Kgnsa3WiPQjc7Bi8k5IBN92cPYfIV9XMFss0=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.39/go.mod h1:GonTDBQ+mTpCVNwaHjj0PagspfrYYMEqOx7FehoEP/I=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40 h1:r5aGipEVgI9aT/tAGjdrPbDQvIAKdTrS3rUPQtG4Rmo=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40/go.mod h1:vOD3CnPxAdkL6MWZeROkZsTlskklMFfgVFkHzx/oZpY=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40 h1:UIXlbijuB2XK1Kr57fo8iIxCuaSHJzwZ1uo+2tbEYIk=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40/go.mod h1:wcEsL6jscjZjVUinb0Q5qD/GXOG1yT3GNfmT9HuDwzU=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40 h1:xLQVRDs2NddDmK9BEyh5KSlJ1Gpy5/GIJXrV6WcVGAE=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40/go.mod h1:XRXnpFVFGLaEVK+olDdFIM1vNa04ETW452oFGEPUxAo=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41 h1:nv/ILuCY0yXACzMQwvtt/HbqDDjemZiI0AeDbxGQlnU=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41/go.mod h1:dzvOSpxaPqQ3j0xS6Lc1vyVuWW0RBj7s/QqYpzu3Q/0=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.10.0 h1:U8/A0RRBaEspzH1uul3JHLbypXwEGUkRkvoT9f0ATcM=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.10.0/go.mod h1:UELStX5KwtJNtQxa+UuF8dc3z4UYc40e8yHYJSozNwY=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40 h1:gr3Fw1cxZXNCdeo/lQ7isHEHzvHVM7z75qb2zW9aMjw=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40/go.mod h1:8z/9CmfnQhiuXD7Ykbcg4a/whSWsniE0ODSx9uwVzfk=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.41 h1:Q9DIKDuJix/oJnQxFpQ26L0EwVa/YNo4k2kbktrjQjE=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.41/go.mod h1:x+TuqkOIG1SZS0+yN54sExGA9ZpjhPO6vPdYnpTFX1M=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.108.0 h1:Yp+x5PKXEmoqHsgP/pAkBy5Tyq1UlXAzM0OInh0vxWw=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.108.0/go.mod h1:locV6DtXyp7Xzr2BG6jtsbeBi3YAWJ/CY4xUThYmIwQ=
+github.com/aws/aws-sdk-go-v2/service/signin v1.6.0 h1:agcr0j8YeFEzdXNo17Rg9MbbjLRjrimabwNtji4e+lU=
+github.com/aws/aws-sdk-go-v2/service/signin v1.6.0/go.mod h1:qU5PxgQ4JiUOOMotzfO3+5oUda5W+8JDVKyLQqlrJik=
+github.com/aws/aws-sdk-go-v2/service/sso v1.34.0 h1:FxaN8/sn61DTXNI6Gt678tFJUY8iUsCchm6Y/F/RjaA=
+github.com/aws/aws-sdk-go-v2/service/sso v1.34.0/go.mod h1:vu4OY6s8LJtT8BtYG2LD6BGSZMptkYn3o5hvCPB22jc=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0 h1:crWKPeGYTBTuBxQ3p73kjfJvt4brUIsr+Fuypko8FxY=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0/go.mod h1:HjjZVhaBz0JBR/kbWKThmNDhFKS7y6EURuk493tJk9Y=
+github.com/aws/aws-sdk-go-v2/service/sts v1.46.0 h1:IZ63JdogSNNjex/jsODNv7jGDcO/xJYd9FsgyfCsp1g=
+github.com/aws/aws-sdk-go-v2/service/sts v1.46.0/go.mod h1:I+rwAf3spG5dITBaAo3xXRowk8kiOhtU1kYxfvCTC44=
+github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ=
+github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
+github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
+github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
+github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
+github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
-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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
+github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
+github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
+github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
+github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 h1:rdnVWKgJpTVXKuKuJyxDJ+NFJdUaUqGvyGy61OcvlbA=
+github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
+github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
+github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
+github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
+github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
+github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
+github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
+github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
+github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
+github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
+github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
+github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
+github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
+github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
+github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
+github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
+github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
+github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
+github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2/v2 v2.7.1 h1:yqDtwI1ptXXvEUNpYTk2lad4jLtAcKqkzepn4savSk4=
+github.com/dlclark/regexp2/v2 v2.7.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
+github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
+github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
+github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
+github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
+github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/elliotchance/orderedmap/v3 v3.1.1 h1:eV7lfZ5fVL8d36b8Wogqi/eqm7R/kZcftA9Yiyj+63M=
+github.com/elliotchance/orderedmap/v3 v3.1.1/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
+github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
+github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
+github.com/envoyproxy/go-control-plane/envoy v1.39.0 h1:1uwRDYPYG8BIBU9Mj1sUAebNmlM6beu/ZKKweSLDxk8=
+github.com/envoyproxy/go-control-plane/envoy v1.39.0/go.mod h1:5e4ylfTZO723MEEFsCpSW4ZEBWR8mwkEyXfwJBTCZ9c=
+github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
+github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
+github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
+github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
+github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
+github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
+github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
+github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
+github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
+github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
+github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
+github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
+github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
+github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/go-task/task/v3 v3.54.0 h1:xDgD9BNHIDnHhemB3gumYb+ozUoa6oCFMIz44GGosj8=
+github.com/go-task/task/v3 v3.54.0/go.mod h1:oOHzpL1r0bga9Xq1dC/8kucE1Y4fxSMTlKKFpNi6mFc=
+github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE=
+github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+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-containerregistry v0.22.1 h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84=
+github.com/google/go-containerregistry v0.22.1/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0=
+github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
+github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
+github.com/google/s2a-go v0.1.10 h1:EMp+aOuXN6l8cE/gjF5Bt+vyZxsUuyCWe9chDWR/+uU=
+github.com/google/s2a-go v0.1.10/go.mod h1:pz4tyvwXvJLLbyrkh6FW1eS2zPUXMaTmyNhYtyP2tNw=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.22 h1:NU4XpII6jD+Dxcot94fqjE+AfJoE/lQP9q3faYGzC/c=
+github.com/googleapis/enterprise-certificate-proxy v0.3.22/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
+github.com/googleapis/gax-go/v2 v2.26.2 h1:ydkmNXxj7bEmmeK5AihkKnWxyOyBR9TDebvp5L5izk8=
+github.com/googleapis/gax-go/v2 v2.26.2/go.mod h1:sMKqnMesnKH+3wiRJROcttA+cJoZoGbZl1vDQ8XYtGk=
+github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 h1:mymLUKThnV9wFvogOK8NnsMP9/vlhnjXY98gr2QIGW8=
+github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74/go.mod h1:Bh9qYL8ehmDxSg14Tk8oxFCP90XHfs6NxV1D84884xA=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-getter v1.8.9 h1:1AOTMUmz/S/GuqOTE6+bg6nwPXTEbKr3Tc/T/lVS7is=
+github.com/hashicorp/go-getter v1.8.9/go.mod h1:qI7vH/m552bXutKe4UkWptOJl4bo2KeO/CSOoh4s0ps=
+github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
+github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/in-toto/attestation v1.2.0 h1:aPRUZ3azbqD7yEBD5fP3TD8Dszf+YHo284SOcpahjQk=
github.com/in-toto/attestation v1.2.0/go.mod h1:r79G45gOmzPismgObLSL+rZTFxUgZLOQJI6LofTZgXk=
-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/spin-stack/go-tools v0.0.0-20261013164036-99d744796066 h1:T4uAeYrQ9e9Fml3PcZGzgji5sIAoebXK9zy94u5IBwg=
-github.com/spin-stack/go-tools v0.0.0-20261013164036-99d744796066/go.mod h1:cy1ojfIbEjrfZi0lGzE+wjueaWw1jrotHQ+vyFz9pZQ=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
+github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
+github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
+github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
+github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
+github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
+github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
+github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
+github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=
+github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
+github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
+github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg=
+github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
+github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
+github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
+github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
+github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
+github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
+github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
+github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
+github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spiffe/go-spiffe/v2 v2.8.1 h1:eXZMLsu+3MLEPJyGJkolqtVrteZfQdUpOWj6LTiDl/E=
+github.com/spiffe/go-spiffe/v2 v2.8.1/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
+github.com/spin-stack/go-tools v0.0.0-20261024191920-5824d9a153be h1:NzmdrDxCGDUvuXDP4TLz7FVJlB71bLI1fFue8MuqL44=
+github.com/spin-stack/go-tools v0.0.0-20261024191920-5824d9a153be/go.mod h1:AjnMnKNXxvNt1n0zTkHpABmBd+NnNBqdRMSwmYu885Y=
+github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
+github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/terminalstatic/go-xsd-validate v0.1.8 h1:UVrTCy1j3DhwaYTTUF+QYO/Nan13S0tf+Jwi+p45Bf0=
github.com/terminalstatic/go-xsd-validate v0.1.8/go.mod h1:1kb47fi2c6onlf+B7UrrQ9VYraOhcYwFm3iG+J6F4Zo=
+github.com/u-root/u-root v0.16.0 h1:wY40O83MBVks97+Is0WlFlOPSwKQMIrWP9R1IsrExg8=
+github.com/u-root/u-root v0.16.0/go.mod h1:yL/XdSSW27PdGLgUh4MNRBy54mKM+TBLzpwiB4nwj90=
+github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
+github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
+github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0=
+github.com/ulikunitz/xz v0.5.16/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
+github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/contrib/detectors/gcp v1.45.0 h1:9jR0ZPRok9ryaOQ2Wx8rg5F7Aon59mxrqbVI60/vlBk=
+go.opentelemetry.io/contrib/detectors/gcp v1.45.0/go.mod h1:VSme3o2fvSg5bVg0dRzyHaj4Z5EVhG+g2Fde6LKzmQA=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0 h1:oECp5f+hN7nkwjU/8BxQ/q23bGPb8FIrD839owX222E=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0/go.mod h1:DqEFwLumhzMBDQv9PcWbyoDxHI/4lAk6CM4nJBH39sc=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04=
+go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
+go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q=
+go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
+go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
+go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
+go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
+go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
+go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
+go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
+go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
+go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
+go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
-golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
-golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
-golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
-golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
+golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
+golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
+golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A=
+golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
+golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
+golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
+golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
+golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
+golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
+golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
+golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
+golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
-golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
-golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
+golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
+golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
+golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
+golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
+golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
+golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
+golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs=
+golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs=
+gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
+gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
+google.golang.org/api v0.299.0 h1:b3K+ydSMd0kh6TQI6bJyApRQfqQX2MfSOaVkpM59mJw=
+google.golang.org/api v0.299.0/go.mod h1:zlR3GVA8b2R5nv5Ij9UWe37StVB3cxDD7DBFi4ZFsHw=
+google.golang.org/genproto v0.0.0-20260724162435-b2f20204f0df h1:A7u56ilVtWR3CySHErsWAsimNwD4FoX9KWd8SUCn6vc=
+google.golang.org/genproto v0.0.0-20260724162435-b2f20204f0df/go.mod h1:4RPHZcXMGptb7aRZb+M43xI7ixmjEtASEf2VtOOBZbU=
+google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df h1:NsJx+hCSwIBI6+C4BuJIkb8xOG1M+nfQDsqIrQHT92k=
+google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459 h1:b0xCahf3FK2m2Cv0p4vTozGPWncCvLfwV86UNg8xWU8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
+google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
+google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
+mvdan.cc/sh/v3 v3.14.1 h1:bXkhQWNHCs0KZEChF8hYS6FC+T2N9mUZLbQv9blditI=
+mvdan.cc/sh/v3 v3.14.1/go.mod h1:syYCoFET8w9tvevxiXUtY8/ICrU+l26jHmhJDra3Vwo=
+mvdan.cc/sh/x v0.0.0-20260922090537-aebdf2b86f56 h1:deNO1TejgB0Dmp3IjB2vkIiHVG81yvLModUSzLjI98A=
+mvdan.cc/sh/x v0.0.0-20260922090537-aebdf2b86f56/go.mod h1:j4gEzJjKcncRAMCu/GVaXCR8/b63sHi/ty4+pG+FuPU=
diff --git a/image/Taskfile.yml b/image/Taskfile.yml
index 4e3f242..c8f8acb 100644
--- a/image/Taskfile.yml
+++ b/image/Taskfile.yml
@@ -23,6 +23,14 @@ tasks:
-v {{.MKOSI_CACHE_DIR}}:/cache \
{{.IMAGE_BUILDER_TAG}}
+ summary:
+ desc: What the built base image is, by size and sha256.
+ cmds:
+ - |
+ set -e
+ ls -la {{.OUTPUT_ABS}}/image/rootfs.qcow2
+ echo "sha256: $(sha256sum {{.OUTPUT_ABS}}/image/rootfs.qcow2 | cut -d' ' -f1)"
+
builder:
desc: Build the container that builds the base image (mkosi, qemu-utils).
# A step of build, not a thing to run on its own.
diff --git a/kernel/Taskfile.yml b/kernel/Taskfile.yml
index f73e1b5..26c1eab 100644
--- a/kernel/Taskfile.yml
+++ b/kernel/Taskfile.yml
@@ -48,6 +48,16 @@ tasks:
echo "$k carries no Xen PVH notes — QEMU has no entry point into it" >&2; exit 1; }
echo "OK: $k, PVH notes present, sha256 $(sha256sum "$k" | cut -c1-16)…"
+ summary:
+ desc: What the built kernel and its config are, by sha256, on the run's summary in CI.
+ cmds:
+ - |
+ set -euo pipefail
+ {
+ echo "kernel sha256: $(sha256sum {{.OUTPUT_ABS}}/kernel/vmlinux | cut -d' ' -f1)"
+ echo "config sha256: $(sha256sum {{.OUTPUT_ABS}}/kernel/kernel-config | cut -d' ' -f1)"
+ } | tee -a "${GITHUB_STEP_SUMMARY:-/dev/null}"
+
menuconfig:
desc: "Edit the kernel config. Writes back to kernel/config--."
interactive: true
diff --git a/qemu/Taskfile.yml b/qemu/Taskfile.yml
index f760d62..efc4c4e 100644
--- a/qemu/Taskfile.yml
+++ b/qemu/Taskfile.yml
@@ -90,6 +90,44 @@ tasks:
- for: [qemu-system-x86_64, qemu-system-x86_64-tcg]
cmd: go run ./hack/machinetype check -qemu {{.OUTPUT_ABS}}/bin/{{.ITEM}}
+ version:
+ desc: The pinned QEMU version, as version=, on the step's outputs in CI.
+ cmds:
+ - echo "version={{.QEMU_VERSION}}" >> "${GITHUB_OUTPUT:-/dev/stdout}"
+
+ summary:
+ desc: >-
+ What the built QEMU is, and whether it still offers the machine type every checkpoint is
+ saved on: on the run's summary in CI.
+ cmds:
+ - |
+ set -euo pipefail
+ {
+ echo "qemu sha256: $(sha256sum {{.OUTPUT_ABS}}/bin/qemu-system-x86_64 | cut -d' ' -f1)"
+ echo "qboot.bin: $(stat -c%s {{.OUTPUT_ABS}}/qemu/qboot.bin) bytes"
+ go run ./hack/machinetype check -qemu {{.OUTPUT_ABS}}/bin/qemu-system-x86_64
+ } | tee -a "${GITHUB_STEP_SUMMARY:-/dev/null}"
+
+ publish:
+ desc: >-
+ Build and push the runtime image, tagged with the pinned version, to GitHub's registry
+ under GITHUB_REPOSITORY: what `fetch` pulls. Needs a docker login that can write packages.
+ cmds:
+ - |
+ set -euo pipefail
+ repo=${GITHUB_REPOSITORY:-spin-stack/spin-machine}
+ docker buildx build \
+ --file qemu/Dockerfile \
+ --target runtime \
+ --platform linux/amd64 \
+ --cache-from type=gha,scope=qemu \
+ $({{.VERSIONS}} args alpine qemu debian debian-snapshot qboot) \
+ --build-arg JOBS=4 \
+ --label org.opencontainers.image.source=https://github.com/${repo} \
+ --tag ghcr.io/${repo}/qemu:{{.QEMU_VERSION}} \
+ --push \
+ .
+
compat:
desc: >-
Print what QEMU's newer machine types change against the pinned one (machine.MachineType):
diff --git a/workflows/workflows_test.go b/workflows/workflows_test.go
index a1084a7..b9da264 100644
--- a/workflows/workflows_test.go
+++ b/workflows/workflows_test.go
@@ -12,16 +12,25 @@ import (
// Every step that publishes runs after an unconditional provenance attestation in its job; the
// ones that look like an upload and ship nothing anyone installs are named here, each with why.
+// A step runs a task, so the tasks that upload and the one that attests are named too: the YAML
+// says only their names.
func TestEveryUploadIsAttested(t *testing.T) {
- g := attestgate.Gate{Root: "..", RequireSBOM: true, MainOnly: []string{"release.yml"}, NotPublished: map[string]string{
- "ci.yml/gate/Upload Go coverage HTML": "a coverage page for a person, kept seven days",
- "kernel.yml/build/Upload the kernel": "a lane's kernel for a person to fetch; a release builds its own",
- "lab.yml/lab/Keep the log": "a measurement's log, for a person",
- "qemu.yml/build/Upload the extracted binaries": "a lane's QEMU for a person to fetch; a release builds its own",
- "qemu.yml/build/Publish the runtime image": "what ci.yml's argument check pulls (task qemu:fetch); a release compiles QEMU, it never takes this",
- "report.yml/report/Keep the report": "the run's copy of report.json, for a person",
- "report.yml/report/Publish the report, and what moved": "report.json is measurement data: nothing installs or verifies it",
- }}
+ g := attestgate.Gate{
+ Root: "..",
+ RequireSBOM: true,
+ MainOnly: []string{"release.yml"},
+ UploadTasks: []string{"qemu:publish", "report:publish"},
+ AttestTasks: []string{"release:sign"},
+ NotPublished: map[string]string{
+ "ci.yml/gate/Upload Go coverage HTML": "a coverage page for a person, kept seven days",
+ "kernel.yml/build/Upload the kernel": "a lane's kernel for a person to fetch; a release builds its own",
+ "lab.yml/lab/Keep the log": "a measurement's log, for a person",
+ "qemu.yml/build/Upload the extracted binaries": "a lane's QEMU for a person to fetch; a release builds its own",
+ "qemu.yml/build/Publish the runtime image": "what ci.yml's argument check pulls (task qemu:fetch); a release compiles QEMU, it never takes this",
+ "report.yml/report/Keep the report": "the run's copy of report.json, for a person",
+ "report.yml/report/Publish the report, and what moved": "report.json is measurement data: nothing installs or verifies it",
+ },
+ }
if err := g.Check(); err != nil {
t.Error(err)
}