Suppress cobra usage dump on non-zero exit codes #1147
Workflow file for this run
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: CI | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| on: | |
| pull_request: | |
| branches: [master, main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Build job gates all tests - nothing runs if build fails | |
| build: | |
| name: Build | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version: '1.24' | |
| cache: true | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libsystemd-dev | |
| - name: Prepare embedded build assets | |
| run: | | |
| mkdir -p internal/image/embedded | |
| mkdir -p internal/config/embedded | |
| cp profiles/default/build.sh internal/image/embedded/coi_build.sh | |
| cp profiles/default/config.toml internal/config/embedded/default_config.toml | |
| cp testdata/dummy/dummy internal/image/embedded/dummy | |
| - name: Run golangci-lint | |
| uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 | |
| with: | |
| version: latest | |
| args: --timeout=5m | |
| - name: Build | |
| run: go build -v ./... | |
| # Go unit tests run in parallel with integration tests (after build passes) | |
| unit-tests: | |
| name: Unit Tests | |
| runs-on: ubuntu-24.04 | |
| needs: build | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version: '1.24' | |
| cache: true | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libsystemd-dev | |
| - name: Prepare embedded build assets | |
| run: | | |
| mkdir -p internal/image/embedded | |
| mkdir -p internal/config/embedded | |
| cp profiles/default/build.sh internal/image/embedded/coi_build.sh | |
| cp profiles/default/config.toml internal/config/embedded/default_config.toml | |
| cp testdata/dummy/dummy internal/image/embedded/dummy | |
| - name: Run unit tests with coverage and race detector | |
| run: | | |
| go test -v -race -coverprofile=coverage.out -covermode=atomic ./... | |
| go tool cover -func=coverage.out | tail -1 # Show total coverage | |
| # Integration tests run in parallel with unit tests (after build passes) | |
| # Split into 6 test groups for parallel execution | |
| # Network isolation uses firewalld (works with any bridge network) | |
| integration: | |
| name: Integration Tests (${{ matrix.test_group.name }}) | |
| runs-on: ubuntu-24.04 | |
| needs: build | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| test_group: | |
| - name: shell-ephemeral | |
| path: tests/shell/ephemeral | |
| description: "Ephemeral shell session tests (17 tests)" | |
| - name: shell-persistent | |
| path: tests/shell/persistent | |
| description: "Persistent shell session tests (9 tests)" | |
| - name: network | |
| path: tests/network | |
| description: "Network isolation tests (10 tests)" | |
| - name: container-file | |
| path: tests/container tests/file | |
| description: "Container and file operations (54 tests)" | |
| - name: core | |
| path: tests/list tests/attach tests/tmux tests/kill tests/run tests/persist tests/build | |
| description: "Core commands: list/attach/tmux/kill/run/persist/build (83 tests)" | |
| - name: misc | |
| path: tests/clean tests/completion tests/config tests/docker tests/errors tests/help tests/image tests/info tests/mount tests/shutdown tests/version tests/meta tests/main_help_flag.py tests/main_help_shorthand.py | |
| description: "Misc commands: clean/completion/config/docker/errors/help/image/info/mount/shutdown/version/meta/main help (72 tests)" | |
| - name: monitoring-nft | |
| path: tests/integration/test_nft_monitoring.py | |
| description: "NFT network monitoring tests (17 tests)" | |
| - name: monitoring-threats | |
| path: tests/integration/test_security_monitoring.py | |
| pytest_args: "-k 'TestThreatDetection or TestEnvironmentScanningPatterns or TestReverseShellPatterns or TestNetworkThreats or TestPromptInjectionScenario or TestMonitoringFeature'" | |
| description: "Threat detection tests (20 tests)" | |
| - name: monitoring-response | |
| path: tests/integration/test_security_monitoring.py | |
| pytest_args: "-k 'TestAutomatedResponse or TestHighLevelThreats or TestLargeWriteDetection or TestMonitoringConfiguration or TestMultipleThreats or TestAuditLogValidation or TestFalsePositives or TestThresholdBoundaries or TestConcurrentThreats'" | |
| description: "Response and threshold tests (22 tests)" | |
| - name: uid-mapping | |
| path: tests/container/uid_mapping_shift_true.py tests/container/uid_mapping_raw_idmap.py | |
| description: "UID mapping integration tests (2 tests)" | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version: '1.24' | |
| cache: true | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libsystemd-dev nftables | |
| # Set up NFT monitoring access for CI | |
| # Add runner to systemd-journal group for log access | |
| sudo usermod -aG systemd-journal $USER | |
| # Configure sudo NOPASSWD for nft commands | |
| echo "$USER ALL=(ALL) NOPASSWD: /usr/sbin/nft *" | sudo tee /etc/sudoers.d/coi-nft | |
| sudo chmod 0440 /etc/sudoers.d/coi-nft | |
| # Verify nft access | |
| sudo -n nft list ruleset || echo "NFT access configured" | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: '3.12' | |
| - name: Cache Python dependencies | |
| uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pip- | |
| - name: Install Python dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r tests/support/requirements.txt | |
| - name: Lint Python code with ruff | |
| run: | | |
| echo "Running ruff linter on Python test code..." | |
| ruff check tests/ | |
| echo "Checking Python code formatting..." | |
| ruff format --check tests/ | |
| - name: Install Incus and Firewalld | |
| run: | | |
| # Try Zabbly repo first (latest Incus), fall back to Ubuntu's native package | |
| sudo mkdir -p /etc/apt/keyrings/ | |
| ZABBLY_SOURCES=/etc/apt/sources.list.d/zabbly-incus-stable.sources | |
| # Attempt to set up Zabbly repo | |
| if sudo curl -fsSL --connect-timeout 10 --retry 2 --retry-delay 5 https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc 2>/dev/null; then | |
| sudo sh -c "cat <<SOURCES > $ZABBLY_SOURCES | |
| Enabled: yes | |
| Types: deb | |
| URIs: https://pkgs.zabbly.com/incus/stable | |
| Suites: \$(. /etc/os-release && echo \${VERSION_CODENAME}) | |
| Components: main | |
| Architectures: \$(dpkg --print-architecture) | |
| Signed-By: /etc/apt/keyrings/zabbly.asc | |
| SOURCES" | |
| echo "Zabbly repo configured" | |
| else | |
| echo "WARNING: Zabbly repo unreachable" | |
| fi | |
| # Install with fallback: try with Zabbly, if it fails remove Zabbly and use Ubuntu native | |
| sudo apt-get update | |
| if ! sudo apt-get install -y incus firewalld; then | |
| echo "WARNING: Install failed, removing Zabbly repo and falling back to Ubuntu native Incus" | |
| sudo rm -f "$ZABBLY_SOURCES" | |
| sudo apt-get update | |
| sudo apt-get install -y incus firewalld | |
| fi | |
| incus version || incus --version || true | |
| - name: Configure Firewalld for network isolation tests | |
| run: | | |
| # Start firewalld for network isolation tests | |
| sudo systemctl enable --now firewalld | |
| # Use trusted zone - it allows all traffic by default, which is what we need | |
| # Our direct rules explicitly REJECT what we want blocked | |
| sudo firewall-cmd --set-default-zone=trusted | |
| # Enable masquerading for container NAT | |
| sudo firewall-cmd --zone=trusted --add-masquerade | |
| # Verify firewalld is running | |
| sudo firewall-cmd --state | |
| sudo firewall-cmd --get-default-zone | |
| echo "Firewalld configured with trusted zone" | |
| - name: Initialize Incus | |
| run: | | |
| # Wait for Incus service to be ready | |
| sudo systemctl start incus.socket || true | |
| sleep 5 | |
| # Configure subuid/subgid for UID mapping (needed for raw.idmap) | |
| # Map runner UID 1001 to be available for container remapping | |
| echo "root:1001:1" | sudo tee -a /etc/subuid | |
| echo "root:1001:1" | sudo tee -a /etc/subgid | |
| # Restart Incus to pick up subuid/subgid changes | |
| sudo systemctl restart incus || true | |
| sleep 5 | |
| # Initialize Incus with btrfs storage and standard bridge networking | |
| cat <<EOF | sudo incus admin init --preseed | |
| config: | |
| images.compression_algorithm: none | |
| networks: | |
| - config: | |
| ipv4.address: 10.47.62.1/24 | |
| ipv4.nat: "true" | |
| ipv6.address: none | |
| name: incusbr0 | |
| type: bridge | |
| storage_pools: | |
| - config: | |
| size: 25GiB | |
| name: default | |
| driver: btrfs | |
| profiles: | |
| - config: {} | |
| devices: | |
| eth0: | |
| name: eth0 | |
| network: incusbr0 | |
| type: nic | |
| root: | |
| path: / | |
| pool: default | |
| type: disk | |
| name: default | |
| EOF | |
| # Allow access without re-login by changing socket permissions | |
| sudo chmod 666 /var/lib/incus/unix.socket | |
| # Add current user to incus-admin group | |
| sudo usermod -aG incus-admin $USER | |
| echo "Bridge network configured successfully" | |
| incus network list | |
| - name: Check kernel idmap support | |
| run: | | |
| echo "Kernel version:" | |
| uname -r | |
| echo "Checking for idmapped mount support:" | |
| grep -i idmap /proc/filesystems || echo "No idmap in /proc/filesystems" | |
| echo "Checking btrfs features:" | |
| sudo btrfs filesystem df /var/lib/incus/storage-pools/default || true | |
| echo "AppArmor status:" | |
| sudo aa-status | grep incus || echo "No incus AppArmor profiles" | |
| echo "Checking if AppArmor is blocking bind mounts:" | |
| sudo dmesg | grep -i "apparmor.*denied" | tail -20 || echo "No recent AppArmor denials" | |
| - name: Configure networking for Incus | |
| run: | | |
| # Enable IP forwarding (required for ALL container networking) | |
| echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward > /dev/null | |
| echo 1 | sudo tee /proc/sys/net/ipv6/conf/all/forwarding > /dev/null | |
| # Set FORWARD chain policy to ACCEPT | |
| # This allows containers without explicit firewall rules to have full network access | |
| # COI's firewall rules use REJECT (not DROP) so they explicitly block traffic | |
| # regardless of the default policy | |
| sudo iptables -P FORWARD ACCEPT | |
| # Ensure NAT/masquerade is working for container internet access | |
| # This is needed in addition to firewalld's masquerade for reliability | |
| DEFAULT_IFACE=$(ip route | grep default | awk '{print $5}' | head -1) | |
| sudo iptables -t nat -A POSTROUTING -s 10.47.62.0/24 -o $DEFAULT_IFACE -j MASQUERADE | |
| echo "Network configuration complete" | |
| echo "FORWARD chain:" | |
| sudo iptables -L FORWARD -n --line-numbers | head -10 | |
| echo "NAT POSTROUTING:" | |
| sudo iptables -t nat -L POSTROUTING -n | |
| - name: Restore base Ubuntu image from cache | |
| id: cache-ubuntu-image | |
| uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 | |
| with: | |
| path: /tmp/ubuntu-base-image.tar.gz | |
| key: ${{ runner.os }}-ubuntu-22.04-base-image | |
| - name: Import or download base Ubuntu image | |
| run: | | |
| if [ -f /tmp/ubuntu-base-image.tar.gz ]; then | |
| echo "Restoring Ubuntu 22.04 base image from cache..." | |
| incus image import /tmp/ubuntu-base-image.tar.gz --alias ubuntu-22.04 | |
| else | |
| echo "Downloading Ubuntu 22.04 base image from remote..." | |
| for attempt in 1 2 3; do | |
| if incus image copy images:ubuntu/22.04 local: --alias ubuntu-22.04 --auto-update=false; then | |
| echo "Image downloaded on attempt $attempt" | |
| # Export for caching | |
| incus image export ubuntu-22.04 /tmp/ubuntu-base-image | |
| break | |
| fi | |
| if [ "$attempt" -eq 3 ]; then | |
| echo "WARNING: Failed to download base image after 3 attempts" | |
| else | |
| echo "Download failed (attempt $attempt/3), retrying in 15s..." | |
| sleep 15 | |
| fi | |
| done | |
| fi | |
| incus image list | |
| - name: Save base Ubuntu image to cache | |
| if: steps.cache-ubuntu-image.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 | |
| with: | |
| path: /tmp/ubuntu-base-image.tar.gz | |
| key: ${{ runner.os }}-ubuntu-22.04-base-image | |
| - name: Prepare embedded build assets | |
| run: | | |
| mkdir -p internal/image/embedded | |
| mkdir -p internal/config/embedded | |
| cp profiles/default/build.sh internal/image/embedded/coi_build.sh | |
| cp profiles/default/config.toml internal/config/embedded/default_config.toml | |
| cp testdata/dummy/dummy internal/image/embedded/dummy | |
| - name: Build COI binary | |
| run: | | |
| go build -o coi ./cmd/coi | |
| ./coi version | |
| - name: Configure COI for CI (use open network mode for builds) | |
| run: | | |
| # Use open mode by default so image builds can access internet | |
| # Tests will explicitly use restricted/allowlist modes when testing network isolation | |
| # Monitoring is NOT enabled globally - monitoring tests use their own enable_monitoring fixture | |
| mkdir -p ~/.coi | |
| cat > ~/.coi/config.toml << 'EOF' | |
| [network] | |
| mode = "open" | |
| EOF | |
| echo "Created COI config with network.mode = open (monitoring disabled by default)" | |
| - name: Test bind mount functionality | |
| continue-on-error: true | |
| run: | | |
| # Skip if base image is not available (e.g., remote was unreachable and no cache) | |
| if ! incus image info ubuntu-22.04 >/dev/null 2>&1; then | |
| echo "WARNING: ubuntu-22.04 image not available, skipping bind mount test" | |
| exit 0 | |
| fi | |
| echo "Testing if bind mounts work with shift=true..." | |
| # Create test directory and file | |
| mkdir -p /tmp/test-bind-mount | |
| echo "test-content" > /tmp/test-bind-mount/test-file.txt | |
| # Launch test container (use local alias to avoid external network dependency) | |
| incus launch ubuntu-22.04 test-bind-mount-container | |
| sleep 5 | |
| # Add bind mount with shift=true | |
| incus config device add test-bind-mount-container testmount disk source=/tmp/test-bind-mount path=/mnt/test shift=true | |
| # Check if file is visible inside container | |
| echo "Files in container /mnt/test:" | |
| incus exec test-bind-mount-container -- ls -la /mnt/test/ | |
| # Try to read the file | |
| echo "Reading file from container:" | |
| incus exec test-bind-mount-container -- cat /mnt/test/test-file.txt || echo "FAILED: Cannot read file in container" | |
| # Try to create a file from inside container | |
| incus exec test-bind-mount-container -- sh -c 'echo "created-from-container" > /mnt/test/created.txt' | |
| # Check if file appears on host | |
| if [ -f /tmp/test-bind-mount/created.txt ]; then | |
| echo "SUCCESS: File created in container is visible on host" | |
| cat /tmp/test-bind-mount/created.txt | |
| else | |
| echo "FAILED: File created in container NOT visible on host" | |
| ls -la /tmp/test-bind-mount/ | |
| fi | |
| # Cleanup | |
| incus delete test-bind-mount-container --force | |
| rm -rf /tmp/test-bind-mount | |
| - name: Restore COI image from cache | |
| id: cache-coi-image | |
| uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 | |
| with: | |
| path: /tmp/coi-image.tar.gz | |
| key: ${{ runner.os }}-coi-image-${{ hashFiles('internal/image/**', 'testdata/dummy/**', 'profiles/default/**') }} | |
| - name: Import COI image from cache | |
| if: steps.cache-coi-image.outputs.cache-hit == 'true' | |
| run: | | |
| echo "Restoring COI image from cache..." | |
| incus image import /tmp/coi-image.tar.gz --alias coi-default | |
| ./coi image list | |
| - name: Build COI image | |
| if: steps.cache-coi-image.outputs.cache-hit != 'true' | |
| run: | | |
| echo "Building COI image (not cached)..." | |
| for attempt in 1 2 3; do | |
| echo "Attempt $attempt of 3..." | |
| if ./coi build; then | |
| echo "Image built successfully on attempt $attempt" | |
| break | |
| fi | |
| if [ "$attempt" -eq 3 ]; then | |
| echo "Failed to build COI image after 3 attempts" | |
| exit 1 | |
| fi | |
| echo "Build failed, retrying in 10s..." | |
| sleep 10 | |
| done | |
| ./coi image list | |
| # Export image for caching | |
| incus image export coi-default /tmp/coi-image | |
| - name: Save COI image to cache | |
| if: steps.cache-coi-image.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 | |
| with: | |
| path: /tmp/coi-image.tar.gz | |
| key: ${{ runner.os }}-coi-image-${{ hashFiles('internal/image/**', 'testdata/dummy/**', 'profiles/default/**') }} | |
| - name: Debug iptables state before tests | |
| run: | | |
| echo "=== FORWARD chain policy and rules ===" | |
| sudo iptables -L FORWARD -n -v --line-numbers | |
| echo "" | |
| echo "=== Firewalld direct rules ===" | |
| sudo firewall-cmd --direct --get-all-rules | |
| echo "" | |
| echo "=== NAT table ===" | |
| sudo iptables -t nat -L -n -v | head -30 | |
| - name: Run ${{ matrix.test_group.name }} tests with coverage | |
| run: | | |
| echo "============================================" | |
| echo "Running: ${{ matrix.test_group.description }}" | |
| echo "Path: ${{ matrix.test_group.path }}" | |
| echo "Args: ${{ matrix.test_group.pytest_args }}" | |
| echo "============================================" | |
| # Run test group with coverage reporting | |
| # pytest_args allows filtering specific test classes within a file | |
| python -m pytest ${{ matrix.test_group.path }} ${{ matrix.test_group.pytest_args }} -v --tb=short --durations=0 --cov=tests --cov-report=term-missing | |
| env: | |
| COI_BINARY: ./coi | |
| GITHUB_REPOSITORY_URL: ${{ github.event.pull_request.head.repo.clone_url || format('https://github.com/{0}.git', github.repository) }} | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| ./coi kill --all --force || true | |
| ./coi clean --force || true | |
| ci-success: | |
| name: CI Success | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| if: always() | |
| needs: | |
| - build | |
| - unit-tests | |
| - integration | |
| steps: | |
| - name: Check all jobs passed | |
| if: | | |
| contains(needs.*.result, 'failure') || | |
| contains(needs.*.result, 'cancelled') || | |
| contains(needs.*.result, 'skipped') | |
| run: exit 1 | |
| - run: echo "All CI checks passed!" |