diff --git a/.github/workflows/publish-agent-helper.yml b/.github/workflows/publish-agent-helper.yml index ac6f68bef..2dbc4dadb 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,17 @@ 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" + 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 - # 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 +67,14 @@ 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' }} + 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 uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 @@ -79,7 +82,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 +90,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..15e9d8344 100644 --- a/hack/agent_helper_image/main.go +++ b/hack/agent_helper_image/main.go @@ -4,11 +4,14 @@ package main import ( + "bytes" "flag" "fmt" "os" "os/exec" "path/filepath" + "strconv" + "strings" ) type target struct { @@ -22,16 +25,39 @@ 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) + case "exists": + err = runExists(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 +83,100 @@ func run(dir string) error { } return nil } + +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) + return writeOutput("value", version) +} + +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 + } + if *image == "" || *tag == "" { + return fmt.Errorf("both -image and -tag are required") + } + + ref := *image + ":" + *tag + 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)) +} + +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 { + msg := strings.TrimSpace(stderr.String()) + if isAbsent(msg) { + return false, nil + } + return false, fmt.Errorf( + "docker manifest inspect %s: %w: %s", ref, err, msg, + ) + } + 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 +} + +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 + } + _, writeErr := fmt.Fprintf(f, "%s=%s\n", key, value) + closeErr := f.Close() + if writeErr != nil { + return writeErr + } + return closeErr +} 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