diff --git a/.github/workflows/build-push.yaml b/.github/workflows/build-push.yaml index a435194..1e82e28 100644 --- a/.github/workflows/build-push.yaml +++ b/.github/workflows/build-push.yaml @@ -1,12 +1,9 @@ name: Build and Push on: - # 1) Trigger on pull request (e.g., to main branch) for a simple build test pull_request: branches: - main - # 2) Trigger on push to main (and optionally only when certain files change), - # plus allow manual runs push: branches: - main @@ -17,6 +14,15 @@ on: - ".devcontainer/**" - ".github/workflows/build-push.yaml" workflow_dispatch: + inputs: + build_amd64: + description: "Release: push linux/amd64 (uncheck to skip). PRs always test both arches in parallel." + type: boolean + default: true + build_arm64: + description: "Release: push linux/arm64 (uncheck to skip). PRs always test both arches in parallel." + type: boolean + default: true permissions: contents: "read" @@ -33,87 +39,195 @@ env: jobs: # ------------------------------------------------------------------------------ - # JOB 1: Build Test for Pull Requests + # PR: parallel build tests (native amd64; arm64 via QEMU) # ------------------------------------------------------------------------------ - build-test: - # Only run this job if the event is a Pull Request + build-test-amd64: if: ${{ github.event_name == 'pull_request' }} runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Build test (linux/amd64) + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/amd64 + push: false + load: true + tags: codecollection-devtools:pr-test-amd64 - - name: Build Test - run: | - echo "Running a quick Docker build test..." - docker build -f Dockerfile . - echo "Build test completed." + build-test-arm64: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - name: Build test (linux/arm64) + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/arm64 + push: false + tags: codecollection-devtools:pr-test-arm64 # ------------------------------------------------------------------------------ - # JOB 2: Build and Push on Push to Main (or manual trigger) + # Release: shared VERSION for per-arch pushes + manifest merge # ------------------------------------------------------------------------------ - build-and-push: - # Only run this job if the event is a 'push' (incl. workflow_dispatch). - # That way PRs won't push images. + prepare-release: if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} steps: - - uses: actions/checkout@v2 - - - id: 'auth-runwhen' - name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v0.4.0' - with: - workload_identity_provider: ${{ secrets.RUNWHEN_NONPROD_SHARED_WI_PROVIDER }} - service_account: ${{ secrets.RUNWHEN_NONPROD_SHARED_WI_SA }} - - - name: Set tag and version - run: |- + - uses: actions/checkout@v4 + - id: version + run: | if [[ -s VERSION ]]; then - VERSION=$(cat VERSION) - echo "VERSION=$VERSION" >> $GITHUB_ENV + VERSION=$(cat VERSION | tr -d '[:space:]') + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "VERSION=$VERSION" >> "$GITHUB_ENV" echo "VERSION extracted: $VERSION" else echo "VERSION file is missing or empty" >&2 exit 1 fi - # Create a 'TAG' environment variable from the branch name + short SHA - echo "TAG=$(echo $GITHUB_REF_NAME | sed 's/[^a-zA-Z0-9]/-/g')-${GITHUB_SHA::8}" >> $GITHUB_ENV + build-push-amd64: + needs: [prepare-release] + if: | + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && + (github.event_name == 'push' || github.event.inputs.build_amd64 == 'true') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - id: auth-runwhen + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0.4.0 + with: + workload_identity_provider: ${{ secrets.RUNWHEN_NONPROD_SHARED_WI_PROVIDER }} + service_account: ${{ secrets.RUNWHEN_NONPROD_SHARED_WI_SA }} - name: Configure docker for GCP run: gcloud --quiet auth configure-docker us-docker.pkg.dev + - name: Login to GHCR + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | \ + docker login --username ${{ github.actor }} --password-stdin ghcr.io + - name: Build & push (linux/amd64) + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.SHARED_ARTIFACT_REPOSITORY_PATH }}/${{ env.IMAGE }}:${{ needs.prepare-release.outputs.version }}-amd64 + ghcr.io/${{ env.GHCR_ORG }}/${{ env.IMAGE }}:${{ needs.prepare-release.outputs.version }}-amd64 + build-args: | + GITHUB_SHA=${{ github.sha }} + GITHUB_REF=${{ github.ref }} + build-push-arm64: + needs: [prepare-release] + if: | + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && + (github.event_name == 'push' || github.event.inputs.build_arm64 == 'true') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - id: auth-runwhen + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0.4.0 + with: + workload_identity_provider: ${{ secrets.RUNWHEN_NONPROD_SHARED_WI_PROVIDER }} + service_account: ${{ secrets.RUNWHEN_NONPROD_SHARED_WI_SA }} + - name: Configure docker for GCP + run: gcloud --quiet auth configure-docker us-docker.pkg.dev - name: Login to GHCR run: | echo "${{ secrets.GITHUB_TOKEN }}" | \ docker login --username ${{ github.actor }} --password-stdin ghcr.io + - name: Build & push (linux/arm64) + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/arm64 + push: true + tags: | + ${{ env.SHARED_ARTIFACT_REPOSITORY_PATH }}/${{ env.IMAGE }}:${{ needs.prepare-release.outputs.version }}-arm64 + ghcr.io/${{ env.GHCR_ORG }}/${{ env.IMAGE }}:${{ needs.prepare-release.outputs.version }}-arm64 + build-args: | + GITHUB_SHA=${{ github.sha }} + GITHUB_REF=${{ github.ref }} - - name: Build & Push - run: |- - docker buildx create --use --name=mybuilder - docker buildx inspect --bootstrap - docker buildx build \ - --push \ - --platform linux/amd64,linux/arm64 \ - --tag "${{ env.SHARED_ARTIFACT_REPOSITORY_PATH }}/${{ env.IMAGE }}:${{ env.VERSION }}" \ - --tag "${{ env.SHARED_ARTIFACT_REPOSITORY_PATH }}/${{ env.IMAGE }}:latest" \ - --tag "ghcr.io/${{ env.GHCR_ORG }}/${{ env.IMAGE }}:${{ env.VERSION }}" \ - --tag "ghcr.io/${{ env.GHCR_ORG }}/${{ env.IMAGE }}:latest" \ - --build-arg GITHUB_SHA="${GITHUB_SHA}" \ - --build-arg GITHUB_REF="${GITHUB_REF}" \ - -f Dockerfile . + merge-manifests: + needs: [prepare-release, build-push-amd64, build-push-arm64] + if: | + always() && !cancelled() && + needs.prepare-release.result == 'success' && + ( + (github.event_name == 'push' && + needs.build-push-amd64.result == 'success' && + needs.build-push-arm64.result == 'success') || + (github.event_name == 'workflow_dispatch' && + (needs.build-push-amd64.result == 'success' || needs.build-push-arm64.result == 'success')) + ) + runs-on: ubuntu-latest + steps: + - id: auth-runwhen + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0.4.0 + with: + workload_identity_provider: ${{ secrets.RUNWHEN_NONPROD_SHARED_WI_PROVIDER }} + service_account: ${{ secrets.RUNWHEN_NONPROD_SHARED_WI_SA }} + - name: Configure docker for GCP + run: gcloud --quiet auth configure-docker us-docker.pkg.dev + - name: Login to GHCR + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | \ + docker login --username ${{ github.actor }} --password-stdin ghcr.io + - uses: docker/setup-buildx-action@v3 + - name: Merge multi-arch manifests (GCP + GHCR) + env: + V: ${{ needs.prepare-release.outputs.version }} + GCP_BASE: ${{ env.SHARED_ARTIFACT_REPOSITORY_PATH }}/${{ env.IMAGE }} + GHCR_BASE: ghcr.io/${{ env.GHCR_ORG }}/${{ env.IMAGE }} + AMD64_OK: ${{ needs.build-push-amd64.result }} + ARM64_OK: ${{ needs.build-push-arm64.result }} + run: | + set -euo pipefail + GCP_REF=() + GHCR_REF=() + if [[ "$AMD64_OK" == "success" ]]; then + GCP_REF+=("${GCP_BASE}:${V}-amd64") + GHCR_REF+=("${GHCR_BASE}:${V}-amd64") + fi + if [[ "$ARM64_OK" == "success" ]]; then + GCP_REF+=("${GCP_BASE}:${V}-arm64") + GHCR_REF+=("${GHCR_BASE}:${V}-arm64") + fi + docker buildx imagetools create \ + -t "${GCP_BASE}:${V}" -t "${GCP_BASE}:latest" \ + "${GCP_REF[@]}" + docker buildx imagetools create \ + -t "${GHCR_BASE}:${V}" -t "${GHCR_BASE}:latest" \ + "${GHCR_REF[@]}" - name: Notify Slack of Container Build - id: slack-publish-nonprod-shared-artifact-repo uses: slackapi/slack-github-action@v1.19.0 with: channel-id: "#notifications" - slack-message: "Just Pushed to ${{ env.SHARED_ARTIFACT_REPOSITORY_PATH }}/${{ env.IMAGE }}:${{ env.VERSION }}" + slack-message: "Just Pushed to ${{ env.SHARED_ARTIFACT_REPOSITORY_PATH }}/${{ env.IMAGE }}:${{ needs.prepare-release.outputs.version }}" env: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - name: Notify Slack of GHCR Push - id: slack-deploy-to-ghcr uses: slackapi/slack-github-action@v1.19.0 with: channel-id: "#codecollections" diff --git a/Dockerfile b/Dockerfile index d68fb89..1635c1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,12 +24,28 @@ RUN usermod -u 1000 runwhen && \ groupmod -g 1000 runwhen ############################################################################### -# Dev tools: sudo, lnav, go-task, terraform +# Dev tools: sudo, gpg/apt deps (bookworm+ has HTTPS in apt; skip apt-transport-https) +# Base image (robot-runtime-base) already ships: curl, ca-certificates, wget, unzip. +# +# Official python:3.14-slim-bookworm can ship sid (or other suites) alongside bookworm. +# Mixing sid + bookworm breaks dpkg (openssl 3.6 vs libssl3 3.0, libgcrypt, etc.). +# Drop all list.d entries and pin /etc/apt/sources.list to bookworm only. ############################################################################### -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - sudo apt-transport-https gnupg2 lsb-release && \ - apt-get clean && \ +COPY docker/apt-ci.conf /etc/apt/apt.conf.d/99docker-ci +ENV DEBIAN_FRONTEND=noninteractive +RUN set -eux; \ + rm -rf /var/lib/apt/lists/*; \ + for f in /etc/apt/sources.list.d/*.sources /etc/apt/sources.list.d/*.list; do \ + if [ -f "$f" ]; then rm -f "$f"; fi; \ + done; \ + printf '%s\n' \ + 'deb http://deb.debian.org/debian bookworm main' \ + 'deb http://deb.debian.org/debian bookworm-updates main' \ + 'deb http://deb.debian.org/debian-security bookworm-security main' \ + > /etc/apt/sources.list; \ + apt-get update; \ + apt-get install -y --no-install-recommends gnupg lsb-release sudo; \ + apt-get clean; \ rm -rf /var/lib/apt/lists/* RUN echo "runwhen ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers @@ -154,7 +170,9 @@ ENV USER="runwhen" RUN pip install --user --no-cache-dir -r requirements.txt ENV PATH="${PATH}:/usr/local/bin:${RUNWHEN_HOME}/.local/bin:${RUNWHEN_HOME}" -ENV PYTHONPATH="$PYTHONPATH:.:${RUNWHEN_HOME}/codecollection/libraries:${RUNWHEN_HOME}/codecollection/codebundles" +# Do not reference $PYTHONPATH here — it is often unset in the base image (BuildKit UndefinedVar). +ENV PYTHONPATH=".:${RUNWHEN_HOME}/codecollection/libraries:${RUNWHEN_HOME}/codecollection/codebundles" EXPOSE 3000 -CMD python -m http.server --bind 0.0.0.0 --directory $ROBOT_LOG_DIR 3000 +# JSON form for clean signals; use literal log dir (same as ENV ROBOT_LOG_DIR). +CMD ["python", "-m", "http.server", "--bind", "0.0.0.0", "--directory", "/robot_logs", "3000"] diff --git a/docker/apt-ci.conf b/docker/apt-ci.conf new file mode 100644 index 0000000..936f1a3 --- /dev/null +++ b/docker/apt-ci.conf @@ -0,0 +1,6 @@ +Acquire::http::Pipeline-Depth "0"; +Acquire::https::Pipeline-Depth "0"; +Acquire::Retries "5"; +Acquire::http::Timeout "120"; +Acquire::https::Timeout "120"; +Acquire::Check-Valid-Until "false";