name: Publish Images

on:
  workflow_dispatch:
    inputs:
      target_arch:
        description: Architecture to publish
        type: choice
        options:
          - all
          - amd64
          - arm64
        default: all
      target_image:
        description: Image to publish
        type: choice
        options:
          - all
          - api
          - executor
          - notifier
          - agent
          - docker-dist
          - web
        default: all
  push:
    branches:
      - main
      - master
    tags:
      - "v*"

env:
  REGISTRY_HOST: ${{ vars.CLUSTER_GITEA_HOST }}
  REGISTRY_NAMESPACE: ${{ vars.CONTAINER_REGISTRY_NAMESPACE }}
  REGISTRY_PLAIN_HTTP: ${{ vars.CONTAINER_REGISTRY_INSECURE }}
  REPOSITORY_NAME: attune
  ARTIFACT_REPOSITORY: attune/build-artifacts
  GNU_GLIBC_VERSION: "2.28"
  CARGO_TERM_COLOR: always
  CARGO_INCREMENTAL: 0
  CARGO_NET_RETRY: 10
  RUSTUP_MAX_RETRIES: 10
  RUST_MIN_STACK: 67108864
  SQLX_OFFLINE: true
  RUNNER_TOOL_CACHE: /toolcache

jobs:
  metadata:
    name: Resolve Publish Metadata
    runs-on: build-amd64
    outputs:
      registry: ${{ steps.meta.outputs.registry }}
      namespace: ${{ steps.meta.outputs.namespace }}
      registry_plain_http: ${{ steps.meta.outputs.registry_plain_http }}
      image_tag: ${{ steps.meta.outputs.image_tag }}
      image_tags: ${{ steps.meta.outputs.image_tags }}
      artifact_ref_base: ${{ steps.meta.outputs.artifact_ref_base }}
    steps:
      - name: Resolve tags and registry paths
        id: meta
        shell: bash
        run: |
          set -euo pipefail

          registry="${REGISTRY_HOST}"
          namespace="${REGISTRY_NAMESPACE}"
          registry_plain_http_raw="${REGISTRY_PLAIN_HTTP:-}"
          registry_host_only="${registry%%:*}"
          registry_plain_http_default="false"

          if [ -z "$registry" ]; then
            echo "CLUSTER_GITEA_HOST app variable is required"
            exit 1
          fi

          if [ -z "$namespace" ]; then
            namespace="${{ github.repository_owner }}"
          fi

          if printf '%s' "$registry_host_only" | grep -Eq '(^|[.])svc[.]cluster[.]local$'; then
            registry_plain_http_default="true"
          fi

          if [ -n "$registry_plain_http_raw" ]; then
            case "$(printf '%s' "$registry_plain_http_raw" | tr '[:upper:]' '[:lower:]')" in
              1|true|yes|on)
                registry_plain_http="true"
                ;;
              0|false|no|off)
                registry_plain_http="false"
                ;;
              *)
                echo "CONTAINER_REGISTRY_INSECURE must be a boolean when set"
                exit 1
                ;;
            esac
          else
            registry_plain_http="$registry_plain_http_default"
          fi

          short_sha="$(printf '%s' "${{ github.sha }}" | cut -c1-12)"
          ref_type="${{ github.ref_type }}"
          ref_name="${{ github.ref_name }}"

          if [ "$ref_type" = "tag" ] && printf '%s' "$ref_name" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([-.].*)?$'; then
            version="${ref_name#v}"
            image_tags="${version},latest,sha-${short_sha}"
          else
            version="sha-${short_sha}"
            image_tags="edge,sha-${short_sha}"
          fi

          artifact_ref_base="${registry}/${namespace}/${ARTIFACT_REPOSITORY}"

          {
            echo "registry=$registry"
            echo "namespace=$namespace"
            echo "registry_plain_http=$registry_plain_http"
            echo "image_tag=$version"
            echo "image_tags=$image_tags"
            echo "artifact_ref_base=$artifact_ref_base"
          } >> "$GITHUB_OUTPUT"

  build-rust-bundles:
    name: Build Rust Bundles (${{ matrix.arch }})
    runs-on: ${{ matrix.runner_label }}
    needs: metadata
    if: |
      github.event_name != 'workflow_dispatch' ||
      inputs.target_arch == 'all' ||
      inputs.target_arch == matrix.arch
    strategy:
      fail-fast: false
      matrix:
        include:
          - arch: amd64
            runner_label: build-amd64
            service_rust_target: x86_64-unknown-linux-gnu
            service_target: x86_64-unknown-linux-gnu.2.28
            musl_target: x86_64-unknown-linux-musl
          - arch: arm64
            runner_label: build-amd64
            service_rust_target: aarch64-unknown-linux-gnu
            service_target: aarch64-unknown-linux-gnu.2.28
            musl_target: aarch64-unknown-linux-musl
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Cache Rust toolchain
        uses: actions/cache@v4
        with:
          path: |
            ~/.rustup/toolchains
            ~/.rustup/update-hashes
          key: rustup-publish-${{ runner.os }}-${{ matrix.arch }}-stable-v1
          restore-keys: |
            rustup-${{ runner.os }}-${{ matrix.arch }}-stable-v1
            rustup-${{ runner.os }}-stable-v1
            rustup-

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: |
            ${{ matrix.service_rust_target }}
            ${{ matrix.musl_target }}

      - name: Cache Cargo registry + index
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry/index
            ~/.cargo/registry/cache
            ~/.cargo/git/db
          key: cargo-registry-publish-${{ matrix.arch }}-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            cargo-registry-publish-${{ matrix.arch }}-
            cargo-registry-

      - name: Cache Cargo build artifacts
        uses: actions/cache@v4
        with:
          path: target
          key: cargo-publish-${{ matrix.arch }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('**/*.rs', '**/Cargo.toml') }}
          restore-keys: |
            cargo-publish-${{ matrix.arch }}-${{ hashFiles('**/Cargo.lock') }}-
            cargo-publish-${{ matrix.arch }}-

      - name: Install native build dependencies
        shell: bash
        run: |
          set -euo pipefail
          apt-get update
          apt-get install -y pkg-config libssl-dev file binutils python3 python3-pip

      - name: Install Zig
        shell: bash
        run: |
          set -euo pipefail
          pip3 install --break-system-packages --no-cache-dir ziglang

      - name: Install cargo-zigbuild
        shell: bash
        run: |
          set -euo pipefail
          if ! command -v cargo-zigbuild >/dev/null 2>&1; then
            cargo install --locked cargo-zigbuild
          fi

      - name: Build release binaries
        shell: bash
        run: |
          set -euo pipefail
          cargo zigbuild --release \
            --target "${{ matrix.service_target }}" \
            --bin attune-api \
            --bin attune-executor \
            --bin attune-notifier

      - name: Verify minimum glibc requirement
        shell: bash
        run: |
          set -euo pipefail
          output_dir="target/${{ matrix.service_rust_target }}/release"

          get_min_glibc() {
            local file_path="$1"
            readelf -W --version-info --dyn-syms "$file_path" \
              | grep 'Name: GLIBC_' \
              | sed -E 's/.*GLIBC_([0-9.]+).*/\1/' \
              | sort -t . -k1,1n -k2,2n \
              | tail -n 1
          }

          version_gt() {
            [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n 1)" = "$1" ] && [ "$1" != "$2" ]
          }

          for binary in attune-api attune-executor attune-notifier; do
            min_glibc="$(get_min_glibc "${output_dir}/${binary}")"
            if [ -z "${min_glibc}" ]; then
              echo "Failed to determine glibc requirement for ${binary}"
              exit 1
            fi
            echo "${binary} requires glibc ${min_glibc}"
            if version_gt "${min_glibc}" "${GNU_GLIBC_VERSION}"; then
              echo "Expected ${binary} to require glibc <= ${GNU_GLIBC_VERSION}, got ${min_glibc}"
              exit 1
            fi
          done

      - name: Build static agent binaries
        shell: bash
        run: |
          set -euo pipefail
          cargo zigbuild --release \
            --target "${{ matrix.musl_target }}" \
            --bin attune-agent \
            --bin attune-sensor-agent

      - name: Assemble binary bundle
        shell: bash
        run: |
          set -euo pipefail

          bundle_root="dist/bundle/${{ matrix.arch }}"
          service_output_dir="target/${{ matrix.service_rust_target }}/release"
          mkdir -p "$bundle_root/bin" "$bundle_root/agent"

          cp "${service_output_dir}/attune-api" "$bundle_root/bin/"
          cp "${service_output_dir}/attune-executor" "$bundle_root/bin/"
          cp "${service_output_dir}/attune-notifier" "$bundle_root/bin/"
          cp target/${{ matrix.musl_target }}/release/attune-agent "$bundle_root/agent/"
          cp target/${{ matrix.musl_target }}/release/attune-sensor-agent "$bundle_root/agent/"

          cat > "$bundle_root/metadata.json" <<EOF
          {
            "git_sha": "${{ github.sha }}",
            "ref": "${{ github.ref }}",
            "arch": "${{ matrix.arch }}",
            "image_tag": "${{ needs.metadata.outputs.image_tag }}"
          }
          EOF

          tar -C dist/bundle/${{ matrix.arch }} -czf "dist/attune-binaries-${{ matrix.arch }}.tar.gz" .

      - name: Setup ORAS
        uses: oras-project/setup-oras@v1

      - name: Log in to registry for artifacts
        shell: bash
        env:
          REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
          REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
          GITHUB_TOKEN_FALLBACK: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          registry_username="${REGISTRY_USERNAME:-${{ github.actor }}}"
          registry_password="${REGISTRY_PASSWORD:-${GITHUB_TOKEN_FALLBACK:-}}"
          login_args=()

          if [ -z "$registry_password" ]; then
            echo "Set CONTAINER_REGISTRY_PASSWORD or enable GITHUB_TOKEN package writes"
            exit 1
          fi

          if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
            login_args+=(--plain-http)
          fi

          oras login "${{ needs.metadata.outputs.registry }}" \
            "${login_args[@]}" \
            --username "$registry_username" \
            --password "$registry_password"

      - name: Push binary bundle artifact
        shell: bash
        run: |
          set -euo pipefail
          push_args=()
          artifact_file="attune-binaries-${{ matrix.arch }}.tar.gz"

          if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
            push_args+=(--plain-http)
          fi

          cp "dist/${artifact_file}" "${artifact_file}"

          oras push \
            "${push_args[@]}" \
            "${{ needs.metadata.outputs.artifact_ref_base }}:rust-binaries-${{ needs.metadata.outputs.image_tag }}-${{ matrix.arch }}" \
            --artifact-type application/vnd.attune.rust-binaries.v1 \
            "${artifact_file}:application/vnd.attune.rust-binaries.layer.v1.tar+gzip"

      - name: Link binary bundle package to repository
        shell: bash
        env:
          REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
          REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
        run: |
          set -euo pipefail
          api_base="${{ github.server_url }}/api/v1"
          package_name="${ARTIFACT_REPOSITORY}"
          encoded_package_name="$(PACKAGE_NAME="${package_name}" python3 -c 'import os, urllib.parse; print(urllib.parse.quote(os.environ["PACKAGE_NAME"], safe=""))')"

          status_code="$(curl -sS -o /tmp/package-link-response.txt -w '%{http_code}' -X POST \
            -u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" \
            "${api_base}/packages/${{ needs.metadata.outputs.namespace }}/container/${encoded_package_name}/-/link/${REPOSITORY_NAME}")"

          case "${status_code}" in
            200|201|204|409)
              ;;
            400|404)
              echo "Package link unsupported for package '${package_name}' on this Gitea endpoint; continuing"
              cat /tmp/package-link-response.txt
              ;;
            *)
              cat /tmp/package-link-response.txt
              exit 1
              ;;
          esac

  publish-docker-dist:
    name: Publish Docker Dist Bundle
    runs-on: build-amd64
    needs: metadata
    if: |
      github.event_name != 'workflow_dispatch' ||
      inputs.target_image == 'all' ||
      inputs.target_image == 'docker-dist'
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Build docker dist bundle
        shell: bash
        run: |
          set -euo pipefail
          bash scripts/package-docker-dist.sh docker/distributable artifacts/attune-docker-dist.tar.gz

      - name: Upload docker dist archive
        uses: actions/upload-artifact@v4
        with:
          name: attune-docker-dist-${{ needs.metadata.outputs.image_tag }}
          path: artifacts/attune-docker-dist.tar.gz
          if-no-files-found: error

      - name: Attach docker dist archive to release
        if: github.ref_type == 'tag'
        shell: bash
        env:
          REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
          REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
        run: |
          set -euo pipefail

          if [ -z "${REGISTRY_USERNAME:-}" ] || [ -z "${REGISTRY_PASSWORD:-}" ]; then
            echo "CONTAINER_REGISTRY_USERNAME and CONTAINER_REGISTRY_PASSWORD are required to attach the docker dist archive to a release"
            exit 1
          fi

          api_base="${{ github.server_url }}/api/v1"
          owner_repo="${{ github.repository }}"
          tag_name="${{ github.ref_name }}"
          archive_path="artifacts/attune-docker-dist.tar.gz"
          asset_name="attune-docker-dist-${tag_name}.tar.gz"

          release_response_file="$(mktemp)"
          status_code="$(curl -sS -o "${release_response_file}" -w '%{http_code}' \
            -u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" \
            "${api_base}/repos/${owner_repo}/releases/tags/${tag_name}")"

          if [ "${status_code}" = "404" ]; then
            create_payload="$(TAG_NAME="${tag_name}" python3 - <<'PY'
            import json
            import os

            tag = os.environ["TAG_NAME"]
            print(json.dumps({
                "tag_name": tag,
                "name": tag,
                "draft": False,
                "prerelease": "-" in tag,
            }))
            PY
            )"

            status_code="$(curl -sS -o "${release_response_file}" -w '%{http_code}' \
              -u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" \
              -H "Content-Type: application/json" \
              -X POST \
              -d "${create_payload}" \
              "${api_base}/repos/${owner_repo}/releases")"
          fi

          case "${status_code}" in
            200|201)
              ;;
            *)
              echo "Failed to fetch or create release for tag ${tag_name}"
              cat "${release_response_file}"
              exit 1
              ;;
          esac

          release_id="$(python3 - "${release_response_file}" <<'PY'
          import json
          import sys

          with open(sys.argv[1], "r", encoding="utf-8") as fh:
              data = json.load(fh)
          print(data["id"])
          PY
          )"

          existing_asset_id="$(python3 - "${release_response_file}" "${asset_name}" <<'PY'
          import json
          import sys

          with open(sys.argv[1], "r", encoding="utf-8") as fh:
              data = json.load(fh)
          name = sys.argv[2]
          for asset in data.get("assets", []):
              if asset.get("name") == name:
                  print(asset["id"])
                  break
          PY
          )"

          if [ -n "${existing_asset_id}" ]; then
            curl -sS \
              -u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" \
              -X DELETE \
              "${api_base}/repos/${owner_repo}/releases/${release_id}/assets/${existing_asset_id}" \
              >/dev/null
          fi

          encoded_asset_name="$(ASSET_NAME="${asset_name}" python3 - <<'PY'
          import os
          import urllib.parse

          print(urllib.parse.quote(os.environ["ASSET_NAME"], safe=""))
          PY
          )"

          upload_response_file="$(mktemp)"
          status_code="$(curl -sS -o "${upload_response_file}" -w '%{http_code}' \
            -u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" \
            -H "Content-Type: application/gzip" \
            --data-binary "@${archive_path}" \
            "${api_base}/repos/${owner_repo}/releases/${release_id}/assets?name=${encoded_asset_name}")"

          case "${status_code}" in
            201)
              ;;
            *)
              echo "Failed to upload release asset ${asset_name}"
              cat "${upload_response_file}"
              exit 1
              ;;
          esac

  publish-rust-images:
    name: Publish ${{ matrix.image.name }} (${{ matrix.arch }})
    runs-on: ${{ matrix.runner_label }}
    needs:
      - metadata
      - build-rust-bundles
    if: |
      (github.event_name != 'workflow_dispatch' ||
      inputs.target_arch == 'all' ||
      inputs.target_arch == matrix.arch) &&
      (github.event_name != 'workflow_dispatch' ||
      inputs.target_image == 'all' ||
      inputs.target_image == matrix.image.name)
    strategy:
      fail-fast: false
      matrix:
        include:
          - arch: amd64
            runner_label: build-amd64
            platform: linux/amd64
            image:
              name: api
              repository: attune/api
              source_path: bin/attune-api
              dockerfile: docker/Dockerfile.runtime
          - arch: amd64
            runner_label: build-amd64
            platform: linux/amd64
            image:
              name: executor
              repository: attune/executor
              source_path: bin/attune-executor
              dockerfile: docker/Dockerfile.runtime
          - arch: amd64
            runner_label: build-amd64
            platform: linux/amd64
            image:
              name: notifier
              repository: attune/notifier
              source_path: bin/attune-notifier
              dockerfile: docker/Dockerfile.runtime
          - arch: amd64
            runner_label: build-amd64
            platform: linux/amd64
            image:
              name: agent
              repository: attune/agent
              source_path: agent/attune-agent
              dockerfile: docker/Dockerfile.agent-package
          - arch: arm64
            runner_label: build-arm64
            platform: linux/arm64
            image:
              name: api
              repository: attune/api
              source_path: bin/attune-api
              dockerfile: docker/Dockerfile.runtime
          - arch: arm64
            runner_label: build-arm64
            platform: linux/arm64
            image:
              name: executor
              repository: attune/executor
              source_path: bin/attune-executor
              dockerfile: docker/Dockerfile.runtime
          - arch: arm64
            runner_label: build-arm64
            platform: linux/arm64
            image:
              name: notifier
              repository: attune/notifier
              source_path: bin/attune-notifier
              dockerfile: docker/Dockerfile.runtime
          - arch: arm64
            runner_label: build-arm64
            platform: linux/arm64
            image:
              name: agent
              repository: attune/agent
              source_path: agent/attune-agent
              dockerfile: docker/Dockerfile.agent-package
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup ORAS
        uses: oras-project/setup-oras@v1

      - name: Setup Docker Buildx
        if: needs.metadata.outputs.registry_plain_http != 'true'
        uses: docker/setup-buildx-action@v3

      - name: Setup Docker Buildx For Plain HTTP Registry
        if: needs.metadata.outputs.registry_plain_http == 'true'
        uses: docker/setup-buildx-action@v3
        with:
          buildkitd-config-inline: |
            [registry."${{ needs.metadata.outputs.registry }}"]
              http = true
              insecure = true

      - name: Log in to registry
        shell: bash
        env:
          REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
          REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
          GITHUB_TOKEN_FALLBACK: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          registry_username="${REGISTRY_USERNAME:-${{ github.actor }}}"
          registry_password="${REGISTRY_PASSWORD:-${GITHUB_TOKEN_FALLBACK:-}}"

          if [ -z "$registry_password" ]; then
            echo "Set CONTAINER_REGISTRY_PASSWORD or enable GITHUB_TOKEN package writes"
            exit 1
          fi

          mkdir -p "$HOME/.docker"
          auth="$(printf '%s:%s' "$registry_username" "$registry_password" | base64 | tr -d '\n')"

          cat > "$HOME/.docker/config.json" <<EOF
          {
            "auths": {
              "${{ needs.metadata.outputs.registry }}": {
                "auth": "${auth}"
              }
            }
          }
          EOF

          oras_login_args=()
          if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
            oras_login_args+=(--plain-http)
          fi

          oras login "${{ needs.metadata.outputs.registry }}" \
            "${oras_login_args[@]}" \
            --username "$registry_username" \
            --password "$registry_password"

      - name: Pull binary bundle
        shell: bash
        run: |
          set -euo pipefail
          pull_args=()
          artifact_ref="${{ needs.metadata.outputs.artifact_ref_base }}:rust-binaries-${{ needs.metadata.outputs.image_tag }}-${{ matrix.arch }}"

          if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
            pull_args+=(--plain-http)
          fi

          echo "Pulling binary bundle artifact"
          echo "  ref: ${artifact_ref}"
          echo "  arch: ${{ matrix.arch }}"
          echo "  plain_http: ${{ needs.metadata.outputs.registry_plain_http }}"

          mkdir -p dist/artifact
          cd dist/artifact

          oras pull \
            "${pull_args[@]}" \
            "${artifact_ref}"

          tar -xzf "attune-binaries-${{ matrix.arch }}.tar.gz"

      - name: Prepare packaging context
        shell: bash
        run: |
          set -euo pipefail
          rm -rf dist/image
          mkdir -p dist/image

          echo "Preparing packaging context"
          echo "  image: ${{ matrix.image.name }}"
          echo "  repository: ${{ matrix.image.repository }}"
          echo "  source_path: ${{ matrix.image.source_path }}"
          echo "  dockerfile: ${{ matrix.image.dockerfile }}"

          case "${{ matrix.image.name }}" in
            api|executor|notifier)
              cp "dist/artifact/${{ matrix.image.source_path }}" dist/attune-service-binary
              ;;
            agent)
              cp dist/artifact/agent/attune-agent dist/attune-agent
              cp dist/artifact/agent/attune-sensor-agent dist/attune-sensor-agent
              ;;
            *)
              echo "Unsupported image: ${{ matrix.image.name }}"
              exit 1
              ;;
          esac

      - name: Push architecture image
        shell: bash
        run: |
          set -euo pipefail

          run_with_retries() {
            local max_attempts="$1"
            local delay_seconds="$2"
            shift 2

            local attempt=1
            while true; do
              if "$@"; then
                return 0
              fi

              if [ "$attempt" -ge "$max_attempts" ]; then
                echo "Command failed after ${attempt} attempts: $*"
                return 1
              fi

              echo "Command failed on attempt ${attempt}/${max_attempts}: $*"
              echo "Retrying in ${delay_seconds}s..."
              sleep "$delay_seconds"
              attempt=$((attempt + 1))
            done
          }

          image_ref="${{ needs.metadata.outputs.registry }}/${{ needs.metadata.outputs.namespace }}/${{ matrix.image.repository }}:${{ needs.metadata.outputs.image_tag }}-${{ matrix.arch }}"

          build_cmd=(
            docker buildx build
            .
            --platform "${{ matrix.platform }}"
            --file "${{ matrix.image.dockerfile }}"
          )

          if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
            build_cmd+=(--output "type=image,\"name=${image_ref}\",push=true,registry.insecure=true")
          else
            build_cmd+=(--tag "$image_ref" --push)
          fi

          echo "Publishing architecture image"
          echo "  image: ${{ matrix.image.name }}"
          echo "  repository: ${{ matrix.image.repository }}"
          echo "  platform: ${{ matrix.platform }}"
          echo "  dockerfile: ${{ matrix.image.dockerfile }}"
          echo "  destination: ${image_ref}"
          echo "  plain_http: ${{ needs.metadata.outputs.registry_plain_http }}"

          run_with_retries 3 5 "${build_cmd[@]}"

      - name: Link container package to repository
        shell: bash
        env:
          REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
          REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
        run: |
          set -euo pipefail
          api_base="${{ github.server_url }}/api/v1"
          package_name="${{ matrix.image.repository }}"
          encoded_package_name="$(PACKAGE_NAME="${package_name}" python3 -c 'import os, urllib.parse; print(urllib.parse.quote(os.environ["PACKAGE_NAME"], safe=""))')"

          status_code="$(curl -sS -o /tmp/package-link-response.txt -w '%{http_code}' -X POST \
            -u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" \
            "${api_base}/packages/${{ needs.metadata.outputs.namespace }}/container/${encoded_package_name}/-/link/${REPOSITORY_NAME}")"

          case "${status_code}" in
            200|201|204|409)
              ;;
            400|404)
              echo "Package link unsupported for package '${package_name}' on this Gitea endpoint; continuing"
              cat /tmp/package-link-response.txt
              ;;
            *)
              cat /tmp/package-link-response.txt
              exit 1
              ;;
          esac

  publish-web-images:
    name: Publish web (${{ matrix.arch }})
    runs-on: ${{ matrix.runner_label }}
    needs: metadata
    if: |
      (github.event_name != 'workflow_dispatch' ||
      inputs.target_arch == 'all' ||
      inputs.target_arch == matrix.arch) &&
      (github.event_name != 'workflow_dispatch' ||
      inputs.target_image == 'all' ||
      inputs.target_image == 'web')
    strategy:
      fail-fast: false
      matrix:
        include:
          - arch: amd64
            runner_label: build-amd64
            platform: linux/amd64
          - arch: arm64
            runner_label: build-arm64
            platform: linux/arm64
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Docker Buildx
        if: needs.metadata.outputs.registry_plain_http != 'true'
        uses: docker/setup-buildx-action@v3

      - name: Setup Docker Buildx For Plain HTTP Registry
        if: needs.metadata.outputs.registry_plain_http == 'true'
        uses: docker/setup-buildx-action@v3
        with:
          buildkitd-config-inline: |
            [registry."${{ needs.metadata.outputs.registry }}"]
              http = true
              insecure = true

      - name: Configure OCI registry auth
        shell: bash
        env:
          REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
          REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
          GITHUB_TOKEN_FALLBACK: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          username="${REGISTRY_USERNAME:-${{ github.actor }}}"
          password="${REGISTRY_PASSWORD:-${GITHUB_TOKEN_FALLBACK:-}}"

          if [ -z "$password" ]; then
            echo "Set CONTAINER_REGISTRY_PASSWORD or enable GITHUB_TOKEN package writes"
            exit 1
          fi

          mkdir -p "$HOME/.docker"
          auth="$(printf '%s:%s' "$username" "$password" | base64 | tr -d '\n')"

          cat > "$HOME/.docker/config.json" <<EOF
          {
            "auths": {
              "${{ needs.metadata.outputs.registry }}": {
                "auth": "${auth}"
              }
            }
          }
          EOF

      - name: Push architecture image
        shell: bash
        run: |
          set -euo pipefail

          run_with_retries() {
            local max_attempts="$1"
            local delay_seconds="$2"
            shift 2

            local attempt=1
            while true; do
              if "$@"; then
                return 0
              fi

              if [ "$attempt" -ge "$max_attempts" ]; then
                echo "Command failed after ${attempt} attempts: $*"
                return 1
              fi

              echo "Command failed on attempt ${attempt}/${max_attempts}: $*"
              echo "Retrying in ${delay_seconds}s..."
              sleep "$delay_seconds"
              attempt=$((attempt + 1))
            done
          }

          image_ref="${{ needs.metadata.outputs.registry }}/${{ needs.metadata.outputs.namespace }}/attune/web:${{ needs.metadata.outputs.image_tag }}-${{ matrix.arch }}"

          build_cmd=(
            docker buildx build
            .
            --platform "${{ matrix.platform }}"
            --file docker/Dockerfile.web
            --provenance=false
            --sbom=false
          )

          if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
            build_cmd+=(--output "type=image,\"name=${image_ref}\",push=true,registry.insecure=true")
          else
            build_cmd+=(--tag "$image_ref" --push)
          fi

          echo "Publishing architecture image"
          echo "  image: web"
          echo "  repository: attune/web"
          echo "  platform: ${{ matrix.platform }}"
          echo "  dockerfile: docker/Dockerfile.web"
          echo "  destination: ${image_ref}"
          echo "  plain_http: ${{ needs.metadata.outputs.registry_plain_http }}"

          run_with_retries 3 5 "${build_cmd[@]}"

      - name: Link web container package to repository
        shell: bash
        env:
          REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
          REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
        run: |
          set -euo pipefail
          api_base="${{ github.server_url }}/api/v1"
          package_name="attune/web"
          encoded_package_name="$(PACKAGE_NAME="${package_name}" python3 -c 'import os, urllib.parse; print(urllib.parse.quote(os.environ["PACKAGE_NAME"], safe=""))')"

          status_code="$(curl -sS -o /tmp/package-link-response.txt -w '%{http_code}' -X POST \
            -u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" \
            "${api_base}/packages/${{ needs.metadata.outputs.namespace }}/container/${encoded_package_name}/-/link/${REPOSITORY_NAME}")"

          case "${status_code}" in
            200|201|204|409)
              ;;
            400|404)
              echo "Package link unsupported for package '${package_name}' on this Gitea endpoint; continuing"
              cat /tmp/package-link-response.txt
              ;;
            *)
              cat /tmp/package-link-response.txt
              exit 1
              ;;
          esac

  publish-manifests:
    name: Publish manifest ${{ matrix.repository }}
    runs-on: build-amd64
    needs:
      - metadata
      - publish-rust-images
      - publish-web-images
    if: |
      github.event_name != 'workflow_dispatch' ||
      (inputs.target_arch == 'all' && inputs.target_image == 'all')
    strategy:
      fail-fast: false
      matrix:
        repository:
          - attune/api
          - attune/executor
          - attune/notifier
          - attune/agent
          - attune/web
    steps:
      - name: Setup Docker Buildx
        if: needs.metadata.outputs.registry_plain_http != 'true'
        uses: docker/setup-buildx-action@v3

      - name: Setup Docker Buildx For Plain HTTP Registry
        if: needs.metadata.outputs.registry_plain_http == 'true'
        uses: docker/setup-buildx-action@v3
        with:
          buildkitd-config-inline: |
            [registry."${{ needs.metadata.outputs.registry }}"]
              http = true
              insecure = true

      - name: Configure OCI registry auth
        shell: bash
        env:
          REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
          REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
          GITHUB_TOKEN_FALLBACK: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          username="${REGISTRY_USERNAME:-${{ github.actor }}}"
          password="${REGISTRY_PASSWORD:-${GITHUB_TOKEN_FALLBACK:-}}"

          if [ -z "$password" ]; then
            echo "Set CONTAINER_REGISTRY_PASSWORD or enable GITHUB_TOKEN package writes"
            exit 1
          fi

          mkdir -p "$HOME/.docker"
          auth="$(printf '%s:%s' "$username" "$password" | base64 | tr -d '\n')"

          cat > "$HOME/.docker/config.json" <<EOF
          {
            "auths": {
              "${{ needs.metadata.outputs.registry }}": {
                "auth": "${auth}"
              }
            }
          }
          EOF

      - name: Publish manifest tags
        shell: bash
        run: |
          set -euo pipefail

          run_with_retries() {
            local max_attempts="$1"
            local delay_seconds="$2"
            shift 2

            local attempt=1
            while true; do
              if "$@"; then
                return 0
              fi

              if [ "$attempt" -ge "$max_attempts" ]; then
                echo "Command failed after ${attempt} attempts: $*"
                return 1
              fi

              echo "Command failed on attempt ${attempt}/${max_attempts}: $*"
              echo "Retrying in ${delay_seconds}s..."
              sleep "$delay_seconds"
              attempt=$((attempt + 1))
            done
          }

          image_base="${{ needs.metadata.outputs.registry }}/${{ needs.metadata.outputs.namespace }}/${{ matrix.repository }}"
          create_args=()
          push_args=()

          if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
            create_args+=(--insecure)
            push_args+=(--insecure)
          fi

          IFS=',' read -ra tags <<< "${{ needs.metadata.outputs.image_tags }}"
          for tag in "${tags[@]}"; do
            manifest_ref="${image_base}:${tag}"
            amd64_ref="${image_base}:${{ needs.metadata.outputs.image_tag }}-amd64"
            arm64_ref="${image_base}:${{ needs.metadata.outputs.image_tag }}-arm64"

            if [ "${{ matrix.repository }}" = "attune/web" ]; then
              echo "Publishing multi-arch manifest with docker manifest"
              echo "  repository: ${{ matrix.repository }}"
              echo "  manifest_tag: ${tag}"
              echo "  manifest_ref: ${manifest_ref}"
              echo "  source_amd64: ${amd64_ref}"
              echo "  source_arm64: ${arm64_ref}"
              echo "  plain_http: ${{ needs.metadata.outputs.registry_plain_http }}"
              docker manifest rm "$manifest_ref" >/dev/null 2>&1 || true
              run_with_retries 3 5 \
                docker manifest create "${create_args[@]}" "$manifest_ref" "$amd64_ref" "$arm64_ref"
              docker manifest annotate "$manifest_ref" "$amd64_ref" --os linux --arch amd64
              docker manifest annotate "$manifest_ref" "$arm64_ref" --os linux --arch arm64
              run_with_retries 3 5 \
                docker manifest push "${push_args[@]}" "$manifest_ref"
            else
              echo "Publishing multi-arch manifest with buildx imagetools"
              echo "  repository: ${{ matrix.repository }}"
              echo "  manifest_tag: ${tag}"
              echo "  manifest_ref: ${manifest_ref}"
              echo "  source_amd64: ${amd64_ref}"
              echo "  source_arm64: ${arm64_ref}"
              echo "  plain_http: ${{ needs.metadata.outputs.registry_plain_http }}"
              run_with_retries 3 5 \
                docker buildx imagetools create \
                --tag "$manifest_ref" \
                "$amd64_ref" \
                "$arm64_ref"
            fi
          done
