refactor(honcho): delegate _is_trivial_prompt wholly to the shared cl… #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Docker Build, Test, and Publish | |
| on: | |
| # Trusted main pushes run this workflow directly so environment-scoped | |
| # Docker Hub secrets are resolved by the top-level workflow, never across | |
| # a reusable-workflow boundary. | |
| push: | |
| branches: [main] | |
| release: | |
| types: [published] | |
| # CI calls this only for untrusted PR build/test coverage. Those runs never | |
| # reach the protected publish or merge jobs below. | |
| workflow_call: | |
| permissions: | |
| contents: read | |
| # Concurrency: push/release runs are NEVER cancelled so every merge gets | |
| # its own image. PR runs reuse a PR-scoped group with | |
| # cancel-in-progress: true so rapid pushes to the same PR collapse to | |
| # the latest commit. | |
| concurrency: | |
| group: docker-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| IMAGE_NAME: nousresearch/hermes-agent | |
| jobs: | |
| # Build and test the image for each architecture. This job runs PR code, | |
| # so it must remain secret-free. Publishing happens in the separate, | |
| # protected publish job after these tests pass. | |
| build: | |
| if: github.repository == 'NousResearch/hermes-agent' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: amd64 | |
| runner: ubuntu-latest | |
| platform: linux/amd64 | |
| cache-from: type=gha,scope=docker-amd64 | |
| cache-to: type=gha,mode=max,scope=docker-amd64 | |
| - arch: arm64 | |
| runner: ubuntu-24.04-arm | |
| platform: linux/arm64 | |
| cache-from: type=gha,scope=docker-arm64 | |
| cache-to: type=gha,mode=max,scope=docker-arm64 | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| # Retry once on transient Docker Hub / buildkit pull failures | |
| # (connection reset, auth token timeout, rate limiting). The action | |
| # generates a unique builder name per invocation so the retry doesn't | |
| # collide with the failed first attempt. A genuine persistent failure | |
| # still fails the job — only the first attempt has continue-on-error. | |
| # Refs: docker/setup-buildx-action#510 | |
| - name: Set up Docker Buildx | |
| id: buildx | |
| continue-on-error: true | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| - name: Set up Docker Buildx (retry) | |
| if: steps.buildx.outcome == 'failure' | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| # Build once, load into the local daemon for testing. Cached | |
| # per-arch; the push step below reuses every layer from this build. | |
| - name: Build image (${{ matrix.arch }}) | |
| uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 | |
| with: | |
| context: . | |
| file: Dockerfile | |
| load: true | |
| platforms: ${{ matrix.platform }} | |
| tags: ${{ env.IMAGE_NAME }}:test | |
| build-args: | | |
| HERMES_GIT_SHA=${{ github.sha }} | |
| cache-from: ${{ matrix.cache-from }} | |
| cache-to: ${{ (github.event_name != 'pull_request') && matrix.cache-to || '' }} | |
| # Run the docker-integration test suite against the freshly-built | |
| # image already loaded into the local daemon (`:test`). | |
| # | |
| # Piggybacking here avoids a second image build: the build step | |
| # already loaded the image into the daemon under | |
| # `${IMAGE_NAME}:test`, so we just point ``HERMES_TEST_IMAGE`` at | |
| # that. The fixture's ``HERMES_TEST_IMAGE`` branch (see | |
| # tests/docker/conftest.py:62-63) short-circuits the rebuild. | |
| # | |
| # Why this job and not a standalone one: the image is 5GB+; passing | |
| # it between jobs via ``docker save``/``upload-artifact`` is slower | |
| # than the build itself. Reusing the existing daemon state is the | |
| # cheapest path to coverage on every PR that touches docker code. | |
| # --------------------------------------------------------------------- | |
| - name: Install uv (for docker tests) | |
| uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # 8.2.0 | |
| with: | |
| # Pinned: unpinned setup-uv fetches a 'latest' manifest from | |
| # raw.githubusercontent.com every job; transient fetch failures | |
| # fail the job (2026-07-28 incident). Keep in sync with tests.yml. | |
| version: "0.9.28" | |
| - name: Set up Python 3.11 (for docker tests) | |
| run: uv python install 3.11 | |
| - name: Install Python dependencies (for docker tests) | |
| # ``dev`` extra pulls in pytest, pytest-asyncio — | |
| # everything tests/docker/ needs. We deliberately avoid ``all`` | |
| # here because the docker tests only drive the container via | |
| # subprocess and don't import hermes_agent's optional deps. | |
| uses: ./.github/actions/retry | |
| with: | |
| command: uv sync --locked --python 3.11 --extra dev | |
| - name: Run docker integration tests | |
| env: | |
| # Skip rebuild; use the image already loaded by the build step. | |
| HERMES_TEST_IMAGE: ${{ env.IMAGE_NAME }}:test | |
| # Match the policy in tests.yml :: test job — no accidental | |
| # real-API calls from inside the harness. | |
| OPENROUTER_API_KEY: "" | |
| OPENAI_API_KEY: "" | |
| NOUS_API_KEY: "" | |
| run: | | |
| scripts/run_tests.sh tests/docker/ --file-timeout 600 | |
| # --------------------------------------------------------------------------- | |
| # Rebuild and push each architecture only after the unprivileged build/test | |
| # matrix passes. This job is the sole Docker Hub credential boundary. | |
| # --------------------------------------------------------------------------- | |
| publish: | |
| if: github.repository == 'NousResearch/hermes-agent' && (github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'release') | |
| needs: [build] | |
| environment: container-publish | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: amd64 | |
| runner: ubuntu-latest | |
| platform: linux/amd64 | |
| cache-from: type=gha,scope=docker-amd64 | |
| cache-to: type=gha,mode=max,scope=docker-amd64 | |
| - arch: arm64 | |
| runner: ubuntu-24.04-arm | |
| platform: linux/arm64 | |
| cache-from: type=gha,scope=docker-arm64 | |
| cache-to: type=gha,mode=max,scope=docker-arm64 | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout trusted source | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| # Retry once on transient Docker Hub / buildkit pull failures. | |
| # See build job for rationale; same pattern. | |
| - name: Set up Docker Buildx | |
| id: buildx | |
| continue-on-error: true | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| - name: Set up Docker Buildx (retry) | |
| if: steps.buildx.outcome == 'failure' | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| # Push by digest only (no tag). The merge job assembles the tagged | |
| # manifest list after both architecture publishers complete. | |
| - name: Push ${{ matrix.arch }} by digest | |
| id: push | |
| uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 | |
| with: | |
| context: . | |
| file: Dockerfile | |
| platforms: ${{ matrix.platform }} | |
| labels: | | |
| org.opencontainers.image.revision=${{ github.sha }} | |
| build-args: | | |
| HERMES_GIT_SHA=${{ github.sha }} | |
| outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | |
| cache-from: ${{ matrix.cache-from }} | |
| cache-to: ${{ matrix.cache-to }} | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.push.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| - name: Upload digest artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: digest-${{ matrix.arch }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # --------------------------------------------------------------------------- | |
| # Stitch both per-arch digests into a single tagged multi-arch manifest. | |
| # This is a registry-side operation — no building, no layer re-push — | |
| # so it runs in ~30 seconds. | |
| # | |
| # On main pushes: tags both :main and :latest. | |
| # On releases: tags :<release_tag_name>. | |
| # --------------------------------------------------------------------------- | |
| merge: | |
| if: github.repository == 'NousResearch/hermes-agent' && (github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'release') | |
| runs-on: ubuntu-latest | |
| needs: [publish] | |
| timeout-minutes: 10 | |
| environment: container-publish | |
| steps: | |
| - name: Download digests | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| path: /tmp/digests | |
| pattern: digest-* | |
| merge-multiple: true | |
| # Retry once on transient Docker Hub / buildkit pull failures. | |
| # See build job for rationale; same pattern. | |
| - name: Set up Docker Buildx | |
| id: buildx | |
| continue-on-error: true | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| - name: Set up Docker Buildx (retry) | |
| if: steps.buildx.outcome == 'failure' | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Create manifest list and push | |
| working-directory: /tmp/digests | |
| env: | |
| IMAGE_NAME: ${{ env.IMAGE_NAME }} | |
| RELEASE_TAG: ${{ github.event.release.tag_name }} | |
| run: | | |
| set -euo pipefail | |
| args=() | |
| for digest_file in *; do | |
| args+=("${IMAGE_NAME}@sha256:${digest_file}") | |
| done | |
| if [ "${{ github.event_name }}" = "release" ]; then | |
| tags=(-t "${IMAGE_NAME}:${RELEASE_TAG}") | |
| else | |
| tags=(-t "${IMAGE_NAME}:main" -t "${IMAGE_NAME}:latest") | |
| fi | |
| # Retry: Docker Hub API + just-pushed digest eventual consistency | |
| # can transiently fail the create; the operation is idempotent. | |
| for i in 1 2 3; do | |
| if docker buildx imagetools create "${tags[@]}" "${args[@]}"; then | |
| break | |
| fi | |
| if [ "$i" = 3 ]; then | |
| echo "::error::imagetools create failed after 3 attempts" | |
| exit 1 | |
| fi | |
| echo "::warning::imagetools create failed (attempt $i); retrying in 20s" | |
| sleep 20 | |
| done | |
| - name: Inspect image | |
| env: | |
| IMAGE_NAME: ${{ env.IMAGE_NAME }} | |
| RELEASE_TAG: ${{ github.event.release.tag_name }} | |
| run: | | |
| if [ "${{ github.event_name }}" = "release" ]; then | |
| docker buildx imagetools inspect "${IMAGE_NAME}:${RELEASE_TAG}" | |
| else | |
| docker buildx imagetools inspect "${IMAGE_NAME}:main" | |
| fi |