Add Dockerfile and entrypoint for devcontainer setup and CI updates #10
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: Dev Container CI | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - '.devcontainer/**' | |
| - '!.devcontainer/screen-shot.png' | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - '.devcontainer/**' | |
| - '!.devcontainer/screen-shot.png' | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }}/devcontainer | |
| permissions: | |
| contents: read | |
| packages: write | |
| jobs: | |
| # ── Job 1: Build the Docker image ──────────────────────────────────── | |
| build: | |
| name: Build Image | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| outputs: | |
| ci_image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:ci-${{ github.run_id }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata (tags, labels) | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=sha,prefix= | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=raw,value=ci-${{ github.run_id }} | |
| - name: Build and push | |
| id: build | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: .devcontainer/Dockerfile | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # ── Job 2: Smoke test the built image ─────────────────────────────── | |
| smoke-test: | |
| name: Smoke Test | |
| needs: build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Pull pre-built image | |
| run: | | |
| echo "Pulling ${{ needs.build.outputs.ci_image }}" | |
| docker pull ${{ needs.build.outputs.ci_image }} | |
| docker tag ${{ needs.build.outputs.ci_image }} hermes-codespace:test | |
| - name: Verify baked tools exist | |
| run: | | |
| docker run --rm --entrypoint bash hermes-codespace:test -c ' | |
| echo "=== Verifying baked tools ===" | |
| PASS=0; FAIL=0 | |
| check() { if eval "$2"; then echo "✅ $1"; PASS=$((PASS+1)); else echo "❌ $1"; FAIL=$((FAIL+1)); fi; } | |
| check "ollama installed" "command -v ollama" | |
| check "hermes installed" "command -v hermes" | |
| check "omniroute installed" "command -v omniroute" | |
| check "modelrelay installed" "command -v modelrelay" | |
| check "mnemon installed" "command -v mnemon" | |
| check "tailscale installed" "command -v tailscale || true" | |
| check "cline installed" "command -v cline" | |
| check "zsh installed" "command -v zsh" | |
| check "ripgrep installed" "command -v rg" | |
| check "entrypoint.sh exists" "[ -x /usr/local/bin/entrypoint.sh ]" | |
| echo "" | |
| echo "=== Results: $PASS passed, $FAIL failed ===" | |
| [ "$FAIL" -eq 0 ] && echo "🎉 All tools baked!" || exit 1 | |
| ' | |
| - name: Verify entrypoint.sh syntax | |
| run: | | |
| docker run --rm --entrypoint bash hermes-codespace:test -c ' | |
| echo "=== Checking entrypoint syntax ===" | |
| bash -n /usr/local/bin/entrypoint.sh && echo "✅ entrypoint.sh syntax OK" | |
| ' | |
| - name: Verify config files copied to image | |
| run: | | |
| docker run --rm --entrypoint bash hermes-codespace:test -c ' | |
| echo "=== Checking config files ===" | |
| [ -f /tmp/devcontainer-config/CLAUDE.md ] && echo "✅ CLAUDE.md present" | |
| [ -f /tmp/devcontainer-config/.claude.json ] && echo "✅ .claude.json present" | |
| [ -f /tmp/devcontainer-config/.hermes.md ] && echo "✅ .hermes.md present" | |
| [ -f /tmp/devcontainer-config/skill-memory-automation.md ] && echo "✅ memory-automation skill present" | |
| ' | |
| # ── Job 3: Full integration test (devcontainer CLI) ────────────────── | |
| integration-test: | |
| name: Integration Test | |
| needs: build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - name: Install devcontainer CLI | |
| run: npm install -g @devcontainers/cli | |
| - name: Pull pre-built image | |
| run: | | |
| docker pull ${{ needs.build.outputs.ci_image }} | |
| docker tag ${{ needs.build.outputs.ci_image }} hermes-codespace:test | |
| - name: Run self-check inside container | |
| run: | | |
| docker run --rm -d --name test-hc \ | |
| -v "$(pwd):/workspace" \ | |
| hermes-codespace:test | |
| echo "Waiting for services to start..." | |
| sleep 30 | |
| docker cp .devcontainer/self-check.sh test-hc:/tmp/self-check.sh | |
| docker exec test-hc bash /tmp/self-check.sh | |
| docker stop test-hc | |
| # ── Job 4: Generate image size report ──────────────────────────────── | |
| image-report: | |
| name: Image Size Report | |
| needs: build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Pull pre-built image | |
| run: | | |
| docker pull ${{ needs.build.outputs.ci_image }} | |
| docker tag ${{ needs.build.outputs.ci_image }} hermes-codespace:test | |
| - name: Report image size | |
| run: | | |
| echo "=== Devcontainer Image Size ===" | |
| docker images hermes-codespace:test --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" | |
| SIZE=$(docker images hermes-codespace:test --format "{{.Size}}") | |
| echo "" | |
| echo "📊 Image size: **$SIZE**" | |
| SIZE_BYTES=$(docker image inspect hermes-codespace:test --format '{{.Size}}') | |
| if [ "$SIZE_BYTES" -gt 4294967296 ]; then | |
| echo "⚠️ WARNING: Image exceeds 4GB — may hit Codespace storage limits" | |
| fi | |
| # ── Job 5: Cleanup temp image from GHCR ───────────────────────────── | |
| cleanup: | |
| name: Cleanup | |
| needs: [build, smoke-test, integration-test, image-report] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Delete temp image tag from GHCR | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| TAG="ci-${{ github.run_id }}" | |
| # URL-encode the package name for the GHCR API | |
| PACKAGE_NAME=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]' | sed 's|/|%2F|g') | |
| echo "Looking for package: ${PACKAGE_NAME}, tag: ${TAG}" | |
| # Get all versions and find the one with our temp tag | |
| VERSIONS=$(gh api "user/packages/container/${PACKAGE_NAME}/versions" --paginate -q '.[].id' 2>/dev/null || true) | |
| FOUND=0 | |
| for VERSION_ID in $VERSIONS; do | |
| TAGS=$(gh api "user/packages/container/${PACKAGE_NAME}/versions/${VERSION_ID}" -q '.metadata.container.tags[]' 2>/dev/null || true) | |
| if echo "$TAGS" | grep -q "^${TAG}$"; then | |
| echo "Deleting version ${VERSION_ID} (tag: ${TAG})..." | |
| gh api -X DELETE "user/packages/container/${PACKAGE_NAME}/versions/${VERSION_ID}" 2>/dev/null || true | |
| FOUND=1 | |
| break | |
| fi | |
| done | |
| if [ "$FOUND" -eq 0 ]; then | |
| echo "No version found with tag ${TAG} — may have been cleaned already" | |
| else | |
| echo "✅ Temp image tag ${TAG} deleted" | |
| fi |