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" </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" < "$HOME/.docker/config.json" < "$HOME/.docker/config.json" </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