name: Docker Image on: # Called by release.yml or other workflows workflow_call: inputs: release: description: "Set true when called from release.yml to publish version/latest tags" required: false type: boolean default: false tag: description: "Image tag override (leave empty for auto-detect)" required: false type: string default: "" # On-demand builds workflow_dispatch: inputs: release: description: "Set true to force release-style tags (:version, :latest, :sha-xxx)" required: false type: boolean default: false tag: description: "Image tag override (leave empty for auto-detect)" required: false type: string default: "" # Staging build from the staging branch every hour schedule: - cron: '0 * * * *' env: IMAGE_NAME: nearaidev/ironclaw WORKER_IMAGE_NAME: nearaidev/ironclaw-worker jobs: build: name: Build & Push runs-on: ubuntu-24.04 permissions: contents: read packages: read actions: write steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: ref: ${{ github.event_name == 'schedule' && 'staging' || '' }} persist-credentials: false - name: Resolve source git commit id: source_sha run: | SOURCE_SHA="$(git rev-parse HEAD)" echo "sha=${SOURCE_SHA}" >> "$GITHUB_OUTPUT" echo "Source commit: ${SOURCE_SHA}" - name: Extract version from Cargo.toml id: version run: | VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then echo "::error::Extracted version '${VERSION}' must match MAJOR.MINOR.PATCH[-prerelease] (Docker tags forbid '+')" exit 1 fi echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "Detected version: ${VERSION}" - name: Determine tags id: tags env: VERSION: ${{ steps.version.outputs.version }} EVENT_NAME: ${{ github.event_name }} IS_RELEASE_BUILD: ${{ inputs.release && 'true' || 'false' }} INPUT_TAG: ${{ inputs.tag }} SOURCE_SHA: ${{ steps.source_sha.outputs.sha }} run: | if [[ -n "${INPUT_TAG}" && ! "${INPUT_TAG}" =~ ^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$ ]]; then echo "::error::Input tag '${INPUT_TAG}' does not match Docker tag grammar" exit 1 fi SHA="sha-${SOURCE_SHA::7}" echo "sha_tag=${SHA}" >> "$GITHUB_OUTPUT" if [[ "${IS_RELEASE_BUILD}" == "true" ]]; then # Release: :version + :latest + :sha-xxx TAGS="${IMAGE_NAME}:${VERSION}" TAGS="${TAGS},${IMAGE_NAME}:latest" TAGS="${TAGS},${IMAGE_NAME}:${SHA}" WORKER_TAGS="${WORKER_IMAGE_NAME}:${VERSION}" WORKER_TAGS="${WORKER_TAGS},${WORKER_IMAGE_NAME}:latest" WORKER_TAGS="${WORKER_TAGS},${WORKER_IMAGE_NAME}:${SHA}" elif [[ "${EVENT_NAME}" == "schedule" ]]; then # Daily staging: :staging + :sha-xxx TAGS="${IMAGE_NAME}:staging" TAGS="${TAGS},${IMAGE_NAME}:${SHA}" WORKER_TAGS="${WORKER_IMAGE_NAME}:staging" WORKER_TAGS="${WORKER_TAGS},${WORKER_IMAGE_NAME}:${SHA}" else # Manual dispatch: :sha-xxx only TAGS="${IMAGE_NAME}:${SHA}" WORKER_TAGS="${WORKER_IMAGE_NAME}:${SHA}" fi # Manual override adds an extra tag (e.g. "staging") if [[ -n "${INPUT_TAG}" ]]; then TAGS="${TAGS},${IMAGE_NAME}:${INPUT_TAG}" WORKER_TAGS="${WORKER_TAGS},${WORKER_IMAGE_NAME}:${INPUT_TAG}" fi echo "tags=${TAGS}" >> "$GITHUB_OUTPUT" echo "worker_tags=${WORKER_TAGS}" >> "$GITHUB_OUTPUT" # Staging builds get pre-bundled WASM extensions if [[ "${EVENT_NAME}" == "schedule" || "${INPUT_TAG}" == "staging" ]]; then echo "target=runtime-staging" >> "$GITHUB_OUTPUT" else echo "target=runtime" >> "$GITHUB_OUTPUT" fi - name: Log in to Docker Hub uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: username: ${{ vars.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} - name: Check if current staging commit is already built id: check if: steps.tags.outputs.target == 'runtime-staging' env: SOURCE_SHA: ${{ steps.source_sha.outputs.sha }} run: | CURRENT_BUILT_SHA="" WORKER_BUILT_SHA="" if docker pull "${IMAGE_NAME}:staging" > /dev/null 2>&1; then CURRENT_BUILT_SHA=$(docker inspect --format='{{index .Config.Labels "ironclaw.git.sha"}}' "${IMAGE_NAME}:staging" 2>/dev/null || echo "") fi if docker pull "${WORKER_IMAGE_NAME}:staging" > /dev/null 2>&1; then WORKER_BUILT_SHA=$(docker inspect --format='{{index .Config.Labels "ironclaw.git.sha"}}' "${WORKER_IMAGE_NAME}:staging" 2>/dev/null || echo "") fi echo "Current built commit (ironclaw): ${CURRENT_BUILT_SHA:-}" echo "Current built commit (ironclaw-worker): ${WORKER_BUILT_SHA:-}" echo "Current source commit: ${SOURCE_SHA}" if [[ -n "${CURRENT_BUILT_SHA}" && "${CURRENT_BUILT_SHA}" == "${SOURCE_SHA}" && -n "${WORKER_BUILT_SHA}" && "${WORKER_BUILT_SHA}" == "${SOURCE_SHA}" ]]; then echo "skip=true" >> "$GITHUB_OUTPUT" echo "Current commit already built for both images — skipping." else echo "skip=false" >> "$GITHUB_OUTPUT" echo "At least one image is missing or out of date — proceeding." fi - name: Set up Docker Buildx if: steps.check.outputs.skip != 'true' uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Build and push (ironclaw) if: steps.check.outputs.skip != 'true' uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . push: true tags: ${{ steps.tags.outputs.tags }} target: ${{ steps.tags.outputs.target }} labels: | ironclaw.git.sha=${{ steps.source_sha.outputs.sha }} platforms: linux/amd64 cache-from: type=gha cache-to: type=gha,mode=max - name: Build and push (ironclaw-worker) if: steps.check.outputs.skip != 'true' uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . file: Dockerfile.worker push: true tags: ${{ steps.tags.outputs.worker_tags }} labels: | ironclaw.git.sha=${{ steps.source_sha.outputs.sha }} platforms: linux/amd64 cache-from: type=gha,scope=worker cache-to: type=gha,mode=max,scope=worker - name: Create releases-manager app token id: app-token if: steps.check.outputs.skip != 'true' continue-on-error: true uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2 with: app-id: ${{ secrets.GH_RELEASES_MANAGER_APP_ID }} private-key: ${{ secrets.GH_RELEASES_MANAGER_APP_PRIVATE_KEY }} owner: nearai repositories: ironclaw-dind - name: Trigger ironclaw-dind Build & Push if: steps.app-token.outcome == 'success' && steps.check.outputs.skip != 'true' continue-on-error: true env: GH_TOKEN: ${{ steps.app-token.outputs.token }} EVENT_NAME: ${{ github.event_name }} IS_RELEASE_BUILD: ${{ inputs.release && 'true' || 'false' }} INPUT_TAG: ${{ inputs.tag }} VERSION: ${{ steps.version.outputs.version }} run: | if [[ "${IS_RELEASE_BUILD}" == "true" && -n "${VERSION}" ]]; then gh api repos/nearai/ironclaw-dind/dispatches \ --method POST \ -f event_type="ironclaw_image_published" \ -f client_payload[version]="${VERSION}" elif [[ "${EVENT_NAME}" == "schedule" ]] || [[ "${INPUT_TAG}" == "staging" ]]; then gh api repos/nearai/ironclaw-dind/dispatches \ --method POST \ -f event_type="ironclaw_image_published" fi - name: Summary if: steps.check.outputs.skip != 'true' env: TAGS: ${{ steps.tags.outputs.tags }} WORKER_TAGS: ${{ steps.tags.outputs.worker_tags }} VERSION: ${{ steps.version.outputs.version }} SOURCE_SHA: ${{ steps.source_sha.outputs.sha }} run: | { echo "## Docker Images" echo "" echo "**ironclaw:**" echo '```' echo "${TAGS}" | tr ',' '\n' echo '```' echo "" echo "**ironclaw-worker:**" echo '```' echo "${WORKER_TAGS}" | tr ',' '\n' echo '```' echo "" echo "- version: \`${VERSION}\`" echo "- sha: \`${SOURCE_SHA}\`" } >> "$GITHUB_STEP_SUMMARY" - name: Summary (skipped) if: steps.check.outputs.skip == 'true' env: SOURCE_SHA: ${{ steps.source_sha.outputs.sha }} run: | { echo "## Docker Images — skipped" echo "" echo "Current commit already built for \`${IMAGE_NAME}:staging\`." echo "- sha: \`${SOURCE_SHA}\`" } >> "$GITHUB_STEP_SUMMARY"