From 33a5f165a03e66f9f69c4dee34433ae7a4d2d909 Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Thu, 23 Apr 2026 15:55:42 -0400 Subject: [PATCH 1/6] introduce a nightly and on push to main github action for the image Signed-off-by: Ryan Cook --- .github/workflows/build-push.yml | 256 +++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 .github/workflows/build-push.yml diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml new file mode 100644 index 0000000..f87c69e --- /dev/null +++ b/.github/workflows/build-push.yml @@ -0,0 +1,256 @@ +name: Build and push bootc image + +on: + push: + branches: [main] + schedule: + - cron: "0 6 * * *" # 06:00 UTC daily + workflow_dispatch: {} + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: quay.io + IMAGE_NAME: sallyom/tank-os + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + platform: [linux/arm64, linux/amd64] + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + + - name: Build image + id: build + uses: redhat-actions/buildah-build@7a95fa7ee0f02d552a32753e7414641a04307056 # v2.13 + with: + image: ${{ env.IMAGE_NAME }} + tags: ${{ github.sha }} + platforms: ${{ matrix.platform }} + containerfiles: bootc/Containerfile + context: bootc + + - name: Push per-arch image to quay.io + uses: redhat-actions/push-to-registry@5ed88d269cf581ea9ef6dd6806d01562096bee9c # v2.8 + with: + image: ${{ env.IMAGE_NAME }} + tags: ${{ github.sha }}-${{ matrix.platform == 'linux/arm64' && 'arm64' || 'amd64' }} + registry: ${{ env.REGISTRY }} + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + + smoke-test: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Log in to quay.io + uses: redhat-actions/podman-login@4934294ad0449894bcd1e9f191899d7292469603 # v1.7 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + + - name: Pull amd64 image + run: podman pull "${REGISTRY}/${IMAGE_NAME}:${COMMIT_SHA}-amd64" + env: + COMMIT_SHA: ${{ github.sha }} + + - name: Validate image layout + shell: bash {0} + run: | + IMAGE="${REGISTRY}/${IMAGE_NAME}:${COMMIT_SHA}-amd64" + failures=0 + + check() { + local desc="$1"; shift + if podman run --rm --entrypoint "" "$IMAGE" bash -c "$*" >/dev/null 2>&1; then + printf ' PASS %s\n' "$desc" + else + printf ' FAIL %s\n' "$desc" + failures=$((failures + 1)) + fi + } + + echo "── File layout ──" + check "openclaw wrapper exists and is executable" \ + 'test -x /usr/local/bin/openclaw' + check "tank-openclaw-secrets exists and is executable" \ + 'test -x /usr/local/bin/tank-openclaw-secrets' + check "tank-os-version exists and is executable" \ + 'test -x /usr/local/bin/tank-os-version' + check "bootstrap-openclaw exists and is executable" \ + 'test -x /usr/libexec/tank-os/bootstrap-openclaw' + check "bootstrap-service-gator exists and is executable" \ + 'test -x /usr/libexec/tank-os/bootstrap-service-gator' + check "sync-podman-secrets exists and is executable" \ + 'test -x /usr/libexec/tank-os/sync-podman-secrets' + check "openclaw.container quadlet exists" \ + 'test -f /etc/containers/systemd/users/1000/openclaw.container' + check "service-gator.container quadlet exists" \ + 'test -f /etc/containers/systemd/users/1000/service-gator.container' + check "sudoers.d/openclaw exists with mode 0440" \ + '[ "$(stat -c %a /etc/sudoers.d/openclaw)" = "440" ]' + check "tank-os-release exists" \ + 'test -f /etc/tank-os-release' + + echo "" + echo "── User and system config ──" + check "openclaw user exists with UID 1000" \ + '[ "$(id -u openclaw)" = "1000" ]' + check "openclaw is in wheel group" \ + 'id -nG openclaw | grep -qw wheel' + check "openclaw home directory exists" \ + 'test -d /var/home/openclaw/.openclaw' + check "linger enabled for openclaw" \ + 'test -f /var/lib/systemd/linger/openclaw' + check "subuid entry for openclaw" \ + 'grep -q "^openclaw:" /etc/subuid' + check "subgid entry for openclaw" \ + 'grep -q "^openclaw:" /etc/subgid' + check "python3 is available" \ + 'command -v python3' + + echo "" + if [ "$failures" -gt 0 ]; then + echo "FAILED: $failures check(s) failed" + exit 1 + fi + echo "All layout checks passed" + env: + COMMIT_SHA: ${{ github.sha }} + + - name: Generate OpenClaw bootstrap config + run: | + OPENCLAW_HOME="${RUNNER_TEMP}/openclaw-home" + mkdir -p "$OPENCLAW_HOME" + IMAGE="${REGISTRY}/${IMAGE_NAME}:${COMMIT_SHA}-amd64" + podman run --rm --entrypoint "" \ + -e HOME=/var/home/openclaw \ + -v "$OPENCLAW_HOME:/var/home/openclaw/.openclaw" \ + "$IMAGE" \ + /usr/libexec/tank-os/bootstrap-openclaw + podman unshare chown -R 1000:1000 "$OPENCLAW_HOME" + echo "Generated config:" + cat "$OPENCLAW_HOME/openclaw.json" + env: + COMMIT_SHA: ${{ github.sha }} + RUNNER_TEMP: ${{ runner.temp }} + + - name: Start OpenClaw container + run: | + podman run -d \ + --name openclaw-smoke \ + --init \ + -v "${RUNNER_TEMP}/openclaw-home:/home/node/.openclaw" \ + -e HOME=/home/node \ + -e TERM=xterm-256color \ + -e NPM_CONFIG_CACHE=/home/node/.openclaw/.npm \ + -e OPENCLAW_NO_RESPAWN=1 \ + -p 18789:18789 \ + -p 18790:18790 \ + ghcr.io/openclaw/openclaw:latest \ + node dist/index.js gateway --allow-unconfigured --bind lan --port 18789 + env: + RUNNER_TEMP: ${{ runner.temp }} + + - name: Wait for OpenClaw to be ready + run: | + echo "Waiting for OpenClaw gateway on port 18789 ..." + for i in $(seq 1 60); do + if curl -sf -o /dev/null http://127.0.0.1:18789/ 2>/dev/null; then + echo "OpenClaw is responding (attempt $i)" + break + fi + if [ "$i" -eq 60 ]; then + echo "Timed out waiting for OpenClaw after 60s" + echo "── container logs ──" + podman logs openclaw-smoke + exit 1 + fi + sleep 1 + done + + - name: Validate OpenClaw is running + run: | + echo "── HTTP response ──" + http_status=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:18789/) + echo "GET / returned HTTP $http_status" + if [ "$http_status" -lt 200 ] || [ "$http_status" -ge 400 ]; then + echo "Unexpected HTTP status" + podman logs openclaw-smoke + exit 1 + fi + + echo "" + echo "── Container state ──" + podman inspect --format '{{.State.Status}}' openclaw-smoke + is_running=$(podman inspect --format '{{.State.Running}}' openclaw-smoke) + if [ "$is_running" != "true" ]; then + echo "Container is not running" + podman logs openclaw-smoke + exit 1 + fi + + echo "" + echo "── podman ps ──" + podman ps --filter name=openclaw-smoke + + echo "" + echo "All application checks passed" + + - name: Cleanup + if: always() + run: podman rm -f openclaw-smoke 2>/dev/null || true + + manifest: + runs-on: ubuntu-latest + needs: [build, smoke-test] + steps: + - name: Determine tag + id: tag + run: | + if [[ "${EVENT_NAME}" == "schedule" ]]; then + echo "tag=nightly" >> "$GITHUB_OUTPUT" + else + echo "tag=latest" >> "$GITHUB_OUTPUT" + fi + env: + EVENT_NAME: ${{ github.event_name }} + + - name: Log in to quay.io + uses: redhat-actions/podman-login@4934294ad0449894bcd1e9f191899d7292469603 # v1.7 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + + - name: Create and push multi-arch manifest + run: | + FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}" + podman manifest create "${FULL_IMAGE}:${MANIFEST_TAG}" + podman manifest add "${FULL_IMAGE}:${MANIFEST_TAG}" "docker://${FULL_IMAGE}:${COMMIT_SHA}-arm64" + podman manifest add "${FULL_IMAGE}:${MANIFEST_TAG}" "docker://${FULL_IMAGE}:${COMMIT_SHA}-amd64" + podman manifest push "${FULL_IMAGE}:${MANIFEST_TAG}" "docker://${FULL_IMAGE}:${MANIFEST_TAG}" + env: + MANIFEST_TAG: ${{ steps.tag.outputs.tag }} + COMMIT_SHA: ${{ github.sha }} + + - name: Clean up per-arch tags + run: | + for arch in arm64 amd64; do + skopeo delete "docker://${REGISTRY}/${IMAGE_NAME}:${COMMIT_SHA}-${arch}" || true + done + env: + COMMIT_SHA: ${{ github.sha }} From 8982dc4429b8c1e71905c3eb12320fc784171402 Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Thu, 23 Apr 2026 16:12:37 -0400 Subject: [PATCH 2/6] items based on feedback Signed-off-by: Ryan Cook --- .github/workflows/build-push.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index f87c69e..679fb7f 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -11,7 +11,7 @@ permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} cancel-in-progress: true env: @@ -21,6 +21,7 @@ env: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 60 strategy: matrix: platform: [linux/arm64, linux/amd64] @@ -51,6 +52,7 @@ jobs: smoke-test: runs-on: ubuntu-latest + timeout-minutes: 20 needs: build steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 @@ -216,6 +218,7 @@ jobs: manifest: runs-on: ubuntu-latest + timeout-minutes: 20 needs: [build, smoke-test] steps: - name: Determine tag From 8a09bc2614f60f565812cc68b6903ff166f7c8ed Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Thu, 23 Apr 2026 16:26:51 -0400 Subject: [PATCH 3/6] repo chagne Signed-off-by: Ryan Cook --- .github/workflows/build-push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 679fb7f..61de484 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -16,7 +16,7 @@ concurrency: env: REGISTRY: quay.io - IMAGE_NAME: sallyom/tank-os + IMAGE_NAME: redhat-et/tank-os jobs: build: From 01403ef185a98b17eb522108096214f7372e8226 Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Sat, 2 May 2026 14:21:22 -0400 Subject: [PATCH 4/6] use native arm64 runners and fix build tag mismatch - Replace QEMU emulation with native GitHub arm64 runners (ubuntu-24.04-arm) so each arch builds natively in parallel - Fix tag mismatch: build step now produces the per-arch suffixed tag that the push step expects - Remove per-arch tag cleanup step from manifest job Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/build-push.yml | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 61de484..3b4bf1b 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -20,23 +20,26 @@ env: jobs: build: - runs-on: ubuntu-latest - timeout-minutes: 60 strategy: matrix: - platform: [linux/arm64, linux/amd64] + include: + - platform: linux/amd64 + arch: amd64 + runner: ubuntu-latest + - platform: linux/arm64 + arch: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - - name: Build image id: build uses: redhat-actions/buildah-build@7a95fa7ee0f02d552a32753e7414641a04307056 # v2.13 with: image: ${{ env.IMAGE_NAME }} - tags: ${{ github.sha }} + tags: ${{ github.sha }}-${{ matrix.arch }} platforms: ${{ matrix.platform }} containerfiles: bootc/Containerfile context: bootc @@ -45,7 +48,7 @@ jobs: uses: redhat-actions/push-to-registry@5ed88d269cf581ea9ef6dd6806d01562096bee9c # v2.8 with: image: ${{ env.IMAGE_NAME }} - tags: ${{ github.sha }}-${{ matrix.platform == 'linux/arm64' && 'arm64' || 'amd64' }} + tags: ${{ github.sha }}-${{ matrix.arch }} registry: ${{ env.REGISTRY }} username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_PASSWORD }} @@ -249,11 +252,3 @@ jobs: env: MANIFEST_TAG: ${{ steps.tag.outputs.tag }} COMMIT_SHA: ${{ github.sha }} - - - name: Clean up per-arch tags - run: | - for arch in arm64 amd64; do - skopeo delete "docker://${REGISTRY}/${IMAGE_NAME}:${COMMIT_SHA}-${arch}" || true - done - env: - COMMIT_SHA: ${{ github.sha }} From 08ea41ccd1f54961b7317f8f1a69054a6df35fd5 Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Mon, 4 May 2026 08:55:40 -0400 Subject: [PATCH 5/6] parse quadlet for smoke test and remove workflow_dispatch - Remove workflow_dispatch trigger to prevent accidental overwrites of the latest tag from non-main branches - Smoke test now extracts Image, Environment, PublishPort, Volume, RunInit, and Exec from the quadlet baked into the built image rather than hardcoding them, so quadlet drift is caught in CI Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/build-push.yml | 46 ++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 3b4bf1b..2c24be4 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -5,7 +5,6 @@ on: branches: [main] schedule: - cron: "0 6 * * *" # 06:00 UTC daily - workflow_dispatch: {} permissions: contents: read @@ -153,21 +152,40 @@ jobs: COMMIT_SHA: ${{ github.sha }} RUNNER_TEMP: ${{ runner.temp }} - - name: Start OpenClaw container + - name: Start OpenClaw container from quadlet run: | - podman run -d \ - --name openclaw-smoke \ - --init \ - -v "${RUNNER_TEMP}/openclaw-home:/home/node/.openclaw" \ - -e HOME=/home/node \ - -e TERM=xterm-256color \ - -e NPM_CONFIG_CACHE=/home/node/.openclaw/.npm \ - -e OPENCLAW_NO_RESPAWN=1 \ - -p 18789:18789 \ - -p 18790:18790 \ - ghcr.io/openclaw/openclaw:latest \ - node dist/index.js gateway --allow-unconfigured --bind lan --port 18789 + IMAGE="${REGISTRY}/${IMAGE_NAME}:${COMMIT_SHA}-amd64" + QUADLET_PATH="/etc/containers/systemd/users/1000/openclaw.container" + QUADLET=$(podman run --rm --entrypoint "" "$IMAGE" cat "$QUADLET_PATH") + + OC_IMAGE=$(echo "$QUADLET" | grep '^Image=' | cut -d= -f2-) + + ARGS="--name openclaw-smoke -d" + + if echo "$QUADLET" | grep -q '^RunInit=true'; then + ARGS="$ARGS --init" + fi + + while IFS= read -r line; do + ARGS="$ARGS -e ${line#Environment=}" + done < <(echo "$QUADLET" | grep '^Environment=') + + while IFS= read -r line; do + ARGS="$ARGS -p ${line#PublishPort=}" + done < <(echo "$QUADLET" | grep '^PublishPort=') + + VOL_DEST=$(echo "$QUADLET" | grep '^Volume=' | cut -d: -f2) + ARGS="$ARGS -v ${RUNNER_TEMP}/openclaw-home:${VOL_DEST}" + + EXEC_CMD=$(echo "$QUADLET" | grep '^Exec=' | sed 's/^Exec=//') + + echo "Parsed from quadlet:" + echo " Image: $OC_IMAGE" + echo " Exec: $EXEC_CMD" + echo "Running: podman run $ARGS $OC_IMAGE $EXEC_CMD" + podman run $ARGS "$OC_IMAGE" $EXEC_CMD env: + COMMIT_SHA: ${{ github.sha }} RUNNER_TEMP: ${{ runner.temp }} - name: Wait for OpenClaw to be ready From 4bf86b2f4e506c14aae4ef4f5af5c7aeaabc49e5 Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Mon, 4 May 2026 09:46:29 -0400 Subject: [PATCH 6/6] add UserNS and User quadlet directives to smoke test Translates UserNS=keep-id to --userns=keep-id and expands User=%U:%G to the runner's UID:GID so the smoke test matches production user/namespace semantics. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/build-push.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 2c24be4..d4c2f55 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -174,6 +174,17 @@ jobs: ARGS="$ARGS -p ${line#PublishPort=}" done < <(echo "$QUADLET" | grep '^PublishPort=') + USERNS=$(echo "$QUADLET" | grep '^UserNS=' | cut -d= -f2-) + if [ -n "$USERNS" ]; then + ARGS="$ARGS --userns=$USERNS" + fi + + USER_DIRECTIVE=$(echo "$QUADLET" | grep '^User=' | cut -d= -f2-) + if [ -n "$USER_DIRECTIVE" ]; then + EXPANDED_USER=$(echo "$USER_DIRECTIVE" | sed "s/%U/$(id -u)/g; s/%G/$(id -g)/g") + ARGS="$ARGS --user $EXPANDED_USER" + fi + VOL_DEST=$(echo "$QUADLET" | grep '^Volume=' | cut -d: -f2) ARGS="$ARGS -v ${RUNNER_TEMP}/openclaw-home:${VOL_DEST}"