From dcaf81acda8e01b68ca513160ca86d2bf6611801 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 26 Jul 2026 22:05:50 -0500 Subject: [PATCH 1/7] ci(agent-helper): version image independently of app release Decouple the agent-helper image version from the main devsy release: track it in images/agent-helper/VERSION and republish only when image sources or that file change (or via manual dispatch). Move version resolution into the agent_helper_image tool as a version subcommand, and skip the push when the resolved tag already exists. --- .github/workflows/publish-agent-helper.yml | 44 +++++++------ hack/agent_helper_image/main.go | 73 ++++++++++++++++++++-- images/agent-helper/VERSION | 1 + 3 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 images/agent-helper/VERSION diff --git a/.github/workflows/publish-agent-helper.yml b/.github/workflows/publish-agent-helper.yml index ac6f68bef..d96584f59 100644 --- a/.github/workflows/publish-agent-helper.yml +++ b/.github/workflows/publish-agent-helper.yml @@ -1,12 +1,16 @@ name: Publish Agent Helper Image on: - release: - types: [published] + push: + branches: [main] + paths: + - images/agent-helper/** + - hack/agent_helper_image/** + - .github/workflows/publish-agent-helper.yml workflow_dispatch: inputs: tag: - description: optional version override (defaults to the latest git tag) + description: optional version override (defaults to images/agent-helper/VERSION) required: false type: string pull_request: @@ -26,8 +30,6 @@ jobs: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - fetch-depth: 0 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: @@ -38,24 +40,15 @@ jobs: version: 0.16.0 - name: cross-compile helper binaries - run: go run ./hack/agent_helper_image + run: go run ./hack/agent_helper_image build - name: resolve version id: version - run: | - set -euo pipefail - if [ -n "${{ inputs.tag }}" ]; then - version="${{ inputs.tag }}" - elif [ "${{ github.event_name }}" = "release" ]; then - version="${{ github.ref_name }}" - else - version="$(git describe --tags --abbrev=0)" - fi - echo "value=$version" >> "$GITHUB_OUTPUT" + run: go run ./hack/agent_helper_image version -tag "${{ inputs.tag }}" - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - # Publish only on release / manual dispatch; pull_request runs build-only. + # Publish on push to main / manual dispatch; pull_request runs build-only. - name: generate GitHub App token id: app-token if: ${{ github.event_name != 'pull_request' }} @@ -72,6 +65,19 @@ jobs: username: ${{ steps.app-token.outputs.app-slug }} password: ${{ steps.app-token.outputs.token }} + - name: check tag exists + id: exists + if: ${{ github.event_name != 'pull_request' }} + run: | + set -euo pipefail + ref="${{ env.IMAGE }}:${{ steps.version.outputs.value }}" + if docker manifest inspect "$ref" >/dev/null 2>&1; then + echo "value=true" >> "$GITHUB_OUTPUT" + echo "::notice::$ref already exists; skipping push" + else + echo "value=false" >> "$GITHUB_OUTPUT" + fi + - name: docker metadata id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 @@ -79,7 +85,7 @@ jobs: images: ${{ env.IMAGE }} tags: | type=raw,value=${{ steps.version.outputs.value }} - type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} + type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }} - name: build and push uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 @@ -87,6 +93,6 @@ jobs: context: images/agent-helper file: images/agent-helper/Dockerfile platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} + push: ${{ github.event_name != 'pull_request' && steps.exists.outputs.value != 'true' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/hack/agent_helper_image/main.go b/hack/agent_helper_image/main.go index cf87f7f87..022b90059 100644 --- a/hack/agent_helper_image/main.go +++ b/hack/agent_helper_image/main.go @@ -1,6 +1,11 @@ // Cross-compiles the agent-helper volume helper for each published arch into // images/agent-helper/dist, producing the per-arch binaries the FROM scratch // image copies in via TARGETARCH. +// +// Subcommands: +// +// build cross-compile the helper binaries (default) +// version resolve the image version and emit value= package main import ( @@ -9,6 +14,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" ) type target struct { @@ -22,16 +28,37 @@ var targets = []target{ } func main() { - dir := flag.String("dir", "images/agent-helper", "agent-helper image directory") - flag.Parse() + args := os.Args[1:] + cmd := "build" + if len(args) > 0 && !strings.HasPrefix(args[0], "-") { + cmd, args = args[0], args[1:] + } - if err := run(*dir); err != nil { + var err error + switch cmd { + case "build": + err = runBuild(args) + case "version": + err = runVersion(args) + default: + err = fmt.Errorf("unknown command %q", cmd) + } + if err != nil { fmt.Fprintln(os.Stderr, "agent_helper_image:", err) os.Exit(1) } } -func run(dir string) error { +func runBuild(args []string) error { + fs := flag.NewFlagSet("build", flag.ExitOnError) + dir := fs.String("dir", "images/agent-helper", "agent-helper image directory") + if err := fs.Parse(args); err != nil { + return err + } + return build(*dir) +} + +func build(dir string) error { distDir := filepath.Join(dir, "dist") if err := os.MkdirAll(distDir, 0o755); err != nil { return err @@ -57,3 +84,41 @@ func run(dir string) error { } return nil } + +// runVersion resolves the image version, preferring an explicit -tag override +// and otherwise reading images/agent-helper/VERSION. It emits value= +// to $GITHUB_OUTPUT when set, so the publish workflow can consume it, and also +// prints the version to stdout. +func runVersion(args []string) error { + fs := flag.NewFlagSet("version", flag.ExitOnError) + dir := fs.String("dir", "images/agent-helper", "agent-helper image directory") + tag := fs.String("tag", "", "explicit version override") + if err := fs.Parse(args); err != nil { + return err + } + + version := strings.TrimSpace(*tag) + if version == "" { + data, err := os.ReadFile(filepath.Join(*dir, "VERSION")) + if err != nil { + return err + } + version = strings.TrimSpace(string(data)) + } + if version == "" { + return fmt.Errorf("agent-helper version is empty") + } + + fmt.Println(version) + if out := os.Getenv("GITHUB_OUTPUT"); out != "" { + f, err := os.OpenFile(out, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer f.Close() + if _, err := fmt.Fprintf(f, "value=%s\n", version); err != nil { + return err + } + } + return nil +} diff --git a/images/agent-helper/VERSION b/images/agent-helper/VERSION new file mode 100644 index 000000000..6e8bf73aa --- /dev/null +++ b/images/agent-helper/VERSION @@ -0,0 +1 @@ +0.1.0 From 0260cdce565f916a21d85f99686e880846594f74 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 26 Jul 2026 22:07:22 -0500 Subject: [PATCH 2/7] ci(agent-helper): move tag-exists check into agent_helper_image tool --- .github/workflows/publish-agent-helper.yml | 10 +--- hack/agent_helper_image/main.go | 53 ++++++++++++++++++---- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/.github/workflows/publish-agent-helper.yml b/.github/workflows/publish-agent-helper.yml index d96584f59..816bb0983 100644 --- a/.github/workflows/publish-agent-helper.yml +++ b/.github/workflows/publish-agent-helper.yml @@ -68,15 +68,7 @@ jobs: - name: check tag exists id: exists if: ${{ github.event_name != 'pull_request' }} - run: | - set -euo pipefail - ref="${{ env.IMAGE }}:${{ steps.version.outputs.value }}" - if docker manifest inspect "$ref" >/dev/null 2>&1; then - echo "value=true" >> "$GITHUB_OUTPUT" - echo "::notice::$ref already exists; skipping push" - else - echo "value=false" >> "$GITHUB_OUTPUT" - fi + run: go run ./hack/agent_helper_image exists -image "${{ env.IMAGE }}" -tag "${{ steps.version.outputs.value }}" - name: docker metadata id: meta diff --git a/hack/agent_helper_image/main.go b/hack/agent_helper_image/main.go index 022b90059..e2ce09321 100644 --- a/hack/agent_helper_image/main.go +++ b/hack/agent_helper_image/main.go @@ -6,6 +6,7 @@ // // build cross-compile the helper binaries (default) // version resolve the image version and emit value= +// exists report whether an image tag already exists in the registry package main import ( @@ -14,6 +15,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" ) @@ -40,6 +42,8 @@ func main() { err = runBuild(args) case "version": err = runVersion(args) + case "exists": + err = runExists(args) default: err = fmt.Errorf("unknown command %q", cmd) } @@ -110,15 +114,44 @@ func runVersion(args []string) error { } fmt.Println(version) - if out := os.Getenv("GITHUB_OUTPUT"); out != "" { - f, err := os.OpenFile(out, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return err - } - defer f.Close() - if _, err := fmt.Fprintf(f, "value=%s\n", version); err != nil { - return err - } + return writeOutput("value", version) +} + +// runExists reports whether image:tag already exists in the registry, emitting +// value=true|false to $GITHUB_OUTPUT so the publish workflow can skip the push +// for an already-published version. It relies on the ambient docker credentials +// established by an earlier registry login. +func runExists(args []string) error { + fs := flag.NewFlagSet("exists", flag.ExitOnError) + image := fs.String("image", "", "image repository (without tag)") + tag := fs.String("tag", "", "image tag to check") + if err := fs.Parse(args); err != nil { + return err } - return nil + if *image == "" || *tag == "" { + return fmt.Errorf("both -image and -tag are required") + } + + ref := *image + ":" + *tag + exists := exec.Command("docker", "manifest", "inspect", ref).Run() == nil + if exists { + fmt.Printf("::notice::%s already exists; skipping push\n", ref) + } + return writeOutput("value", strconv.FormatBool(exists)) +} + +// writeOutput appends key=value to $GITHUB_OUTPUT when running under GitHub +// Actions; otherwise it is a no-op. +func writeOutput(key, value string) error { + out := os.Getenv("GITHUB_OUTPUT") + if out == "" { + return nil + } + f, err := os.OpenFile(out, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer f.Close() + _, err = fmt.Fprintf(f, "%s=%s\n", key, value) + return err } From 91b9d61b20058a88b727b1b3b2549ae90ac636dd Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 26 Jul 2026 22:14:58 -0500 Subject: [PATCH 3/7] ci(agent-helper): check f.Close error in writeOutput --- hack/agent_helper_image/main.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hack/agent_helper_image/main.go b/hack/agent_helper_image/main.go index e2ce09321..86ff0cad5 100644 --- a/hack/agent_helper_image/main.go +++ b/hack/agent_helper_image/main.go @@ -151,7 +151,9 @@ func writeOutput(key, value string) error { if err != nil { return err } - defer f.Close() - _, err = fmt.Fprintf(f, "%s=%s\n", key, value) - return err + if _, err := fmt.Fprintf(f, "%s=%s\n", key, value); err != nil { + f.Close() + return err + } + return f.Close() } From 824c5e1c8f0b86facb76124a93eacaf5b05840d6 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 26 Jul 2026 22:21:52 -0500 Subject: [PATCH 4/7] ci(agent-helper): check both f.Close paths in writeOutput --- hack/agent_helper_image/main.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/hack/agent_helper_image/main.go b/hack/agent_helper_image/main.go index 86ff0cad5..1505595d1 100644 --- a/hack/agent_helper_image/main.go +++ b/hack/agent_helper_image/main.go @@ -151,9 +151,10 @@ func writeOutput(key, value string) error { if err != nil { return err } - if _, err := fmt.Fprintf(f, "%s=%s\n", key, value); err != nil { - f.Close() - return err + _, writeErr := fmt.Fprintf(f, "%s=%s\n", key, value) + closeErr := f.Close() + if writeErr != nil { + return writeErr } - return f.Close() + return closeErr } From e43a59e7e91269506e3fb5a189ead080f7676960 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 26 Jul 2026 22:46:52 -0500 Subject: [PATCH 5/7] ci(agent-helper): harden version/exists steps per review Bind untrusted workflow_dispatch input and step outputs to env vars in the run steps to avoid shell injection. In the exists check, distinguish an absent tag/package from unexpected (network/transient) failures instead of treating every docker error as absence. --- .github/workflows/publish-agent-helper.yml | 9 ++++- hack/agent_helper_image/main.go | 46 +++++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-agent-helper.yml b/.github/workflows/publish-agent-helper.yml index 816bb0983..2dbc4dadb 100644 --- a/.github/workflows/publish-agent-helper.yml +++ b/.github/workflows/publish-agent-helper.yml @@ -44,7 +44,9 @@ jobs: - name: resolve version id: version - run: go run ./hack/agent_helper_image version -tag "${{ inputs.tag }}" + env: + INPUT_TAG: ${{ inputs.tag }} + run: go run ./hack/agent_helper_image version -tag "$INPUT_TAG" - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 @@ -68,7 +70,10 @@ jobs: - name: check tag exists id: exists if: ${{ github.event_name != 'pull_request' }} - run: go run ./hack/agent_helper_image exists -image "${{ env.IMAGE }}" -tag "${{ steps.version.outputs.value }}" + env: + IMAGE: ${{ env.IMAGE }} + VERSION: ${{ steps.version.outputs.value }} + run: go run ./hack/agent_helper_image exists -image "$IMAGE" -tag "$VERSION" - name: docker metadata id: meta diff --git a/hack/agent_helper_image/main.go b/hack/agent_helper_image/main.go index 1505595d1..ad9d43edb 100644 --- a/hack/agent_helper_image/main.go +++ b/hack/agent_helper_image/main.go @@ -10,6 +10,7 @@ package main import ( + "bytes" "flag" "fmt" "os" @@ -133,13 +134,56 @@ func runExists(args []string) error { } ref := *image + ":" + *tag - exists := exec.Command("docker", "manifest", "inspect", ref).Run() == nil + exists, err := manifestExists(ref) + if err != nil { + return err + } if exists { fmt.Printf("::notice::%s already exists; skipping push\n", ref) } return writeOutput("value", strconv.FormatBool(exists)) } +// manifestExists reports whether ref resolves in the registry. A response +// indicating the tag or package is absent yields (false, nil); any other +// failure (network, rate limit, transient registry outage) is returned as an +// error so it is not silently misread as an available tag. +// +// This runs after a push-scoped registry login, so a "denied"/"unauthorized" +// response means the package does not exist yet (e.g. the first publish) rather +// than a credential problem — bad credentials would already have failed the +// login step. Those are therefore treated as absence. +func manifestExists(ref string) (bool, error) { + var stderr bytes.Buffer + cmd := exec.Command("docker", "manifest", "inspect", ref) + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + if isAbsent(stderr.String()) { + return false, nil + } + return false, fmt.Errorf("docker manifest inspect %s: %w: %s", ref, err, strings.TrimSpace(stderr.String())) + } + return true, nil +} + +func isAbsent(stderr string) bool { + s := strings.ToLower(stderr) + for _, marker := range []string{ + "no such manifest", + "manifest unknown", + "name unknown", + "not found", + "denied", + "unauthorized", + "forbidden", + } { + if strings.Contains(s, marker) { + return true + } + } + return false +} + // writeOutput appends key=value to $GITHUB_OUTPUT when running under GitHub // Actions; otherwise it is a no-op. func writeOutput(key, value string) error { From 26c1a661001430103c0f58ae1e20fd457051cdb4 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 26 Jul 2026 22:47:53 -0500 Subject: [PATCH 6/7] ci(agent-helper): drop explanatory comments in agent_helper_image --- hack/agent_helper_image/main.go | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/hack/agent_helper_image/main.go b/hack/agent_helper_image/main.go index ad9d43edb..6cb543cc8 100644 --- a/hack/agent_helper_image/main.go +++ b/hack/agent_helper_image/main.go @@ -1,12 +1,6 @@ // Cross-compiles the agent-helper volume helper for each published arch into // images/agent-helper/dist, producing the per-arch binaries the FROM scratch // image copies in via TARGETARCH. -// -// Subcommands: -// -// build cross-compile the helper binaries (default) -// version resolve the image version and emit value= -// exists report whether an image tag already exists in the registry package main import ( @@ -90,10 +84,6 @@ func build(dir string) error { return nil } -// runVersion resolves the image version, preferring an explicit -tag override -// and otherwise reading images/agent-helper/VERSION. It emits value= -// to $GITHUB_OUTPUT when set, so the publish workflow can consume it, and also -// prints the version to stdout. func runVersion(args []string) error { fs := flag.NewFlagSet("version", flag.ExitOnError) dir := fs.String("dir", "images/agent-helper", "agent-helper image directory") @@ -118,10 +108,6 @@ func runVersion(args []string) error { return writeOutput("value", version) } -// runExists reports whether image:tag already exists in the registry, emitting -// value=true|false to $GITHUB_OUTPUT so the publish workflow can skip the push -// for an already-published version. It relies on the ambient docker credentials -// established by an earlier registry login. func runExists(args []string) error { fs := flag.NewFlagSet("exists", flag.ExitOnError) image := fs.String("image", "", "image repository (without tag)") @@ -144,15 +130,6 @@ func runExists(args []string) error { return writeOutput("value", strconv.FormatBool(exists)) } -// manifestExists reports whether ref resolves in the registry. A response -// indicating the tag or package is absent yields (false, nil); any other -// failure (network, rate limit, transient registry outage) is returned as an -// error so it is not silently misread as an available tag. -// -// This runs after a push-scoped registry login, so a "denied"/"unauthorized" -// response means the package does not exist yet (e.g. the first publish) rather -// than a credential problem — bad credentials would already have failed the -// login step. Those are therefore treated as absence. func manifestExists(ref string) (bool, error) { var stderr bytes.Buffer cmd := exec.Command("docker", "manifest", "inspect", ref) @@ -184,8 +161,6 @@ func isAbsent(stderr string) bool { return false } -// writeOutput appends key=value to $GITHUB_OUTPUT when running under GitHub -// Actions; otherwise it is a no-op. func writeOutput(key, value string) error { out := os.Getenv("GITHUB_OUTPUT") if out == "" { From a5d12a648a6ddec826e13308d85df466ab80335d Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 26 Jul 2026 22:50:57 -0500 Subject: [PATCH 7/7] ci(agent-helper): wrap long line for golines --- hack/agent_helper_image/main.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hack/agent_helper_image/main.go b/hack/agent_helper_image/main.go index 6cb543cc8..15e9d8344 100644 --- a/hack/agent_helper_image/main.go +++ b/hack/agent_helper_image/main.go @@ -135,10 +135,13 @@ func manifestExists(ref string) (bool, error) { cmd := exec.Command("docker", "manifest", "inspect", ref) cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - if isAbsent(stderr.String()) { + msg := strings.TrimSpace(stderr.String()) + if isAbsent(msg) { return false, nil } - return false, fmt.Errorf("docker manifest inspect %s: %w: %s", ref, err, strings.TrimSpace(stderr.String())) + return false, fmt.Errorf( + "docker manifest inspect %s: %w: %s", ref, err, msg, + ) } return true, nil }