diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml new file mode 100644 index 000000000..34b4e30b7 --- /dev/null +++ b/.github/workflows/docker-build.yaml @@ -0,0 +1,97 @@ +name: Docker Build + +on: + workflow_dispatch: + inputs: + tag_latest: + description: 'Tag as latest' + required: true + type: choice + options: + - 'true' + - 'false' + default: 'false' + push: + # temp disable branch filter for testing + #branches: [main] + paths: + - 'docker/**' + - 'scripts/setup_*.sh' + - 'src/holosoma_inference/docker/Dockerfile' + - 'src/holosoma_retargeting/docker/Dockerfile' + +permissions: + contents: read + +env: + ECR_REPO: 982423663241.dkr.ecr.us-west-2.amazonaws.com + +jobs: + build: + strategy: + fail-fast: false # continue on one build failing + matrix: + include: + # CPU-buildable images + - environment: inference + dockerfile: 'src/holosoma_inference/docker/Dockerfile' + runner_prefix: 'codebuild-holosoma-cpu-build' + - environment: retargeting + dockerfile: 'src/holosoma_retargeting/docker/Dockerfile' + runner_prefix: 'codebuild-holosoma-cpu-build' + # GPU-required images (setup scripts build/link against CUDA/IsaacSim) + - environment: holosoma + dockerfile: 'docker/Dockerfile' + runner_prefix: 'codebuild-holosoma-a10g-x1-gpu-build' + - environment: isaacgym + dockerfile: 'docker/isaacgym.Dockerfile' + runner_prefix: 'codebuild-holosoma-a10g-x1-gpu-build' + - environment: isaacsim + dockerfile: 'docker/isaacsim.Dockerfile' + runner_prefix: 'codebuild-holosoma-a10g-x1-gpu-build' + - environment: mujoco + dockerfile: 'docker/mujoco.Dockerfile' + runner_prefix: 'codebuild-holosoma-a10g-x1-gpu-build' + #TODO: add mujoco warp + name: Build ${{ matrix.environment }} Docker Image + continue-on-error: true + runs-on: ${{ matrix.runner_prefix }}-${{ github.run_id }}-${{ github.run_attempt }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Generate Image name and path + id: name + # image name should be `holosoma` or `holsoma-${environment}` + run: | + IMAGE_NAME="${{ matrix.environment == 'holosoma' && '' || 'holosoma-' }}${{ matrix.environment }}" + IMAGE_PATH="${{ env.ECR_REPO }}/${IMAGE_NAME}" + echo "image_path=${IMAGE_PATH}" >> "$GITHUB_OUTPUT" + + # might be unnecessary + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Generate image tags + id: tags + run: | + tags="${{ steps.name.outputs.image_path }}:$(date -u +%Y_%m%d_%H%M)" + if [[ "${{ inputs.tag_latest || 'false' }}" == "true" ]]; then + tags="$tags,${{ steps.name.outputs.image_path }}:latest" + fi + echo "tags=$tags" >> "$GITHUB_OUTPUT" + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: "${{ matrix.dockerfile }}" + push: true + tags: "${{ steps.tags.outputs.tags }}" + cache-from: type=registry,ref=${{ steps.name.outputs.image_path }}:buildcache + cache-to: type=registry,ref=${{ steps.name.outputs.image_path }}:buildcache,mode=max + + - name: Summary + run: | + echo "## Docker Build Summary" >> "$GITHUB_STEP_SUMMARY" + echo "- **Tags:** \`${{ steps.tags.outputs.tags }}\`" >> "$GITHUB_STEP_SUMMARY" diff --git a/docker/build_and_push.sh b/docker/build_and_push.sh index 80b17a8df..9dec378cd 100755 --- a/docker/build_and_push.sh +++ b/docker/build_and_push.sh @@ -5,7 +5,7 @@ # Usage: # bash docker/build_and_push.sh # build & push all images # bash docker/build_and_push.sh mujoco retarget # only matching images -# bash docker/build_and_push.sh --no-push # build only, skip push +# bash docker/build_and_push.sh --latest # also tag and push as :latest # bash docker/build_and_push.sh --dry-run # print commands, do nothing # # Environment variables: @@ -34,13 +34,13 @@ IMAGES=( ) # ── Parse flags ────────────────────────────────────────────────────── -NO_PUSH=false +TAG_LATEST=false DRY_RUN=false FILTERS=() for arg in "$@"; do case "$arg" in - --no-push) NO_PUSH=true ;; + --latest) TAG_LATEST=true ;; --dry-run) DRY_RUN=true ;; --help|-h) sed -n '2,/^$/{ s/^# \?//; p }' "$0" @@ -81,32 +81,30 @@ for cmd in docker; do fi done -if ! $NO_PUSH && ! $DRY_RUN; then +if ! $DRY_RUN; then if ! command -v aws &>/dev/null; then echo "Error: aws CLI is not installed (required for push)" >&2; exit 1 fi fi # ── ECR authentication ────────────────────────────────────────────── -if ! $NO_PUSH; then - echo "==> Configuring ECR credential helper for ${ECR_REPO}" - if ! $DRY_RUN; then - # Ensure docker config dir exists - mkdir -p ~/.docker - - # Add ecr-login credential helper for our registry if not already present - if [ ! -f ~/.docker/config.json ]; then - echo '{}' > ~/.docker/config.json - fi +echo "==> Configuring ECR credential helper for ${ECR_REPO}" +if ! $DRY_RUN; then + # Ensure docker config dir exists + mkdir -p ~/.docker + + # Add ecr-login credential helper for our registry if not already present + if [ ! -f ~/.docker/config.json ]; then + echo '{}' > ~/.docker/config.json + fi - if ! jq -e ".credHelpers[\"${ECR_REPO}\"]" ~/.docker/config.json &>/dev/null; then - tmp=$(mktemp) - jq --arg repo "$ECR_REPO" '.credHelpers[$repo] = "ecr-login"' ~/.docker/config.json > "$tmp" \ - && mv "$tmp" ~/.docker/config.json - echo " Added ecr-login credential helper for ${ECR_REPO}" - else - echo " ECR credential helper already configured" - fi + if ! jq -e ".credHelpers[\"${ECR_REPO}\"]" ~/.docker/config.json &>/dev/null; then + tmp=$(mktemp) + jq --arg repo "$ECR_REPO" '.credHelpers[$repo] = "ecr-login"' ~/.docker/config.json > "$tmp" \ + && mv "$tmp" ~/.docker/config.json + echo " Added ecr-login credential helper for ${ECR_REPO}" + else + echo " ECR credential helper already configured" fi fi @@ -125,30 +123,31 @@ for entry in "${IMAGES[@]}"; do echo "" echo "==> Building ${image_name} from ${dockerfile}" - # tags: date tag + latest - tags=(-t "${full_image}:latest" -t "${full_image}:${TAG}") + # tags: always date tag, optionally latest + tags=(-t "${full_image}:${TAG}") + if $TAG_LATEST; then + tags+=(-t "${full_image}:latest") + fi if run env DOCKER_BUILDKIT=1 docker build "${tags[@]}" -f "${dockerfile}" "${ROOT_REPO}"; then echo " Built: ${image_name}" - if ! $NO_PUSH; then - echo " Pushing ${image_name}..." - push_ok=true + echo " Pushing ${image_name}..." + push_ok=true + if ! run docker push "${full_image}:${TAG}"; then + push_ok=false + fi + if $TAG_LATEST; then if ! run docker push "${full_image}:latest"; then push_ok=false fi - if ! run docker push "${full_image}:${TAG}"; then - push_ok=false - fi + fi - if $push_ok; then - SUCCEEDED+=("$image_name") - else - echo " FAILED to push: ${image_name}" >&2 - FAILED+=("$image_name") - fi - else + if $push_ok; then SUCCEEDED+=("$image_name") + else + echo " FAILED to push: ${image_name}" >&2 + FAILED+=("$image_name") fi else echo " FAILED to build: ${image_name}" >&2 @@ -196,6 +195,6 @@ if [[ ${#SUCCEEDED[@]} -eq 0 ]] && [[ ${#FAILED[@]} -eq 0 ]]; then exit 1 fi echo " Tag: ${TAG}" -$NO_PUSH && echo " (push skipped — --no-push)" +$TAG_LATEST && echo " (also tagged as :latest)" $DRY_RUN && echo " (dry run — nothing was executed)" echo " Done." diff --git a/src/holosoma_inference/holosoma_inference/utils/network.py b/src/holosoma_inference/holosoma_inference/utils/network.py index a8802991f..b9950c489 100644 --- a/src/holosoma_inference/holosoma_inference/utils/network.py +++ b/src/holosoma_inference/holosoma_inference/utils/network.py @@ -1,13 +1,13 @@ """Network interface auto-detection for robot communication.""" -import os +from pathlib import Path _SKIP_PREFIXES = ("lo", "wl", "docker", "br-", "veth", "virbr", "vnet", "tun", "tap") def detect_robot_interface() -> str: """Return the name of the single wired NIC that is operationally UP.""" - for ifname in sorted(os.listdir("/sys/class/net/")): + for ifname in sorted(Path("/sys/class/net/").iterdir()): if any(ifname.startswith(p) for p in _SKIP_PREFIXES): continue try: