fix(ci): correctly wire three-image CI after inspecting image layouts #97
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: Integration Tests | |
| # Runs on a GitHub-hosted windows-latest runner. | |
| # WSL2 runs three containers via Docker daemon: | |
| # - extension-kit builder: clones source into /tmp/ext-src, runs make all | |
| # - adaptixc2 detached server, --network host, built ext-kit + patched profile mounted | |
| # - testing-kit runs feature validation + integration tests | |
| # SSH delivery goes from Docker(WSL) → Windows. | |
| # Beacon callbacks go from Windows → Docker(WSL). | |
| on: | |
| push: | |
| pull_request: | |
| workflow_dispatch: | |
| env: | |
| CI_USER: ci_runner | |
| CI_PASS: Ci_Test_Pass1! | |
| CI_AGENT_DIR: 'C:\ci' | |
| CI_AGENT_PATH: 'C:\ci\agent.exe' | |
| ADAPTIXC2_IMAGE: ghcr.io/tgjls/adaptixc2:latest | |
| EXTENSION_KIT_IMAGE: ghcr.io/tgjls/extension-kit:latest | |
| TESTING_KIT_IMAGE: ghcr.io/tgjls/testing-kit:latest | |
| jobs: | |
| integration-test: | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Pass CI variables into WSL | |
| shell: powershell | |
| run: echo "WSLENV=CI_USER/u:CI_PASS/u:CI_AGENT_PATH/u" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| # ── Windows: CI user, OpenSSH, tmp directory ───────────────────────────── | |
| - name: Create CI user | |
| shell: powershell | |
| run: | | |
| $pass = ConvertTo-SecureString $env:CI_PASS -AsPlainText -Force | |
| if (-not (Get-LocalUser $env:CI_USER -ErrorAction SilentlyContinue)) { | |
| New-LocalUser $env:CI_USER -Password $pass -PasswordNeverExpires | |
| Add-LocalGroupMember -Group Administrators -Member $env:CI_USER | |
| } else { | |
| Set-LocalUser $env:CI_USER -Password $pass | |
| } | |
| - name: Start OpenSSH Server with password auth | |
| shell: powershell | |
| run: | | |
| $cap = Get-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 | |
| if ($cap.State -ne 'Installed') { | |
| Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 | |
| } | |
| Set-Service sshd -StartupType Automatic | |
| Start-Service sshd | |
| $cfg = "$env:ProgramData\ssh\sshd_config" | |
| (Get-Content $cfg) ` | |
| -replace '^#?PasswordAuthentication\s+\w+', 'PasswordAuthentication yes' | | |
| Set-Content $cfg | |
| Restart-Service sshd | |
| - name: Create tmp directory for SSH key transfer | |
| shell: powershell | |
| run: New-Item -ItemType Directory -Force -Path C:\tmp | Out-Null | |
| # ── WSL: Ubuntu + Docker ────────────────────────────────────────────────── | |
| - uses: Vampire/setup-wsl@v7 | |
| with: | |
| distribution: Ubuntu-24.04 | |
| additional-packages: docker.io iproute2 curl git | |
| - name: Start Docker daemon in WSL | |
| shell: wsl-bash {0} | |
| run: | | |
| sudo update-alternatives --set iptables /usr/sbin/iptables-legacy || true | |
| sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy || true | |
| sudo dockerd > /dev/null 2>&1 & | |
| echo "Waiting for Docker..." | |
| for i in $(seq 1 30); do | |
| sudo docker info >/dev/null 2>&1 && { echo "Docker ready."; break; } | |
| sleep 1 | |
| done | |
| sudo docker info >/dev/null 2>&1 || { echo "Docker failed to start"; exit 1; } | |
| # ── WSL: SSH setup ──────────────────────────────────────────────────────── | |
| - name: Generate SSH keypair | |
| shell: wsl-bash {0} | |
| run: | | |
| ssh-keygen -t ed25519 -N "" -f ~/.ssh/ci_key | |
| cp ~/.ssh/ci_key.pub /mnt/c/tmp/ci_key.pub | |
| - name: Install SSH public key on Windows | |
| shell: powershell | |
| run: | | |
| $authFile = "$env:ProgramData\ssh\administrators_authorized_keys" | |
| New-Item -Force -ItemType File -Path $authFile | Out-Null | |
| Get-Content C:\tmp\ci_key.pub | Add-Content $authFile | |
| icacls $authFile /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F" | |
| # ── WSL: CI config ──────────────────────────────────────────────────────── | |
| - name: Write CI config | |
| shell: wsl-bash {0} | |
| run: | | |
| WSL_IP=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' | head -1) | |
| WSL_GW=$(ip route show default 2>/dev/null | awk 'NR==1{print $3}') | |
| WINDOWS_IP=$(cmd.exe /c ipconfig 2>/dev/null | tr -d '\r' | awk '/vEthernet.*WSL/{f=1} f && /IPv4 Address/{print $NF; exit}') | |
| if [ -z "$WINDOWS_IP" ] || ip addr show 2>/dev/null | grep -qF "$WINDOWS_IP"; then | |
| CALLBACK_HOST="127.0.0.1" | |
| SSH_HOST="127.0.0.1" | |
| echo "=== WSL2 mirrored mode: WSL_IP=$WSL_IP WINDOWS_IP=${WINDOWS_IP:-none}, using localhost ===" | |
| else | |
| CALLBACK_HOST="$WSL_IP" | |
| SSH_HOST="${WSL_GW:-$WINDOWS_IP}" | |
| echo "=== WSL2 NAT mode: WSL_IP=$WSL_IP WSL_GW=$WSL_GW WINDOWS_IP=$WINDOWS_IP, SSH→$SSH_HOST ===" | |
| fi | |
| cat > /tmp/ci_config.yaml << EOF | |
| server: | |
| url: https://127.0.0.1:4321 | |
| endpoint: /endpoint | |
| operator: | |
| name: ci | |
| password: pass | |
| setup: | |
| project: ci | |
| agent_output: /tmp/ci_agent.exe | |
| listener: | |
| name: ci_http | |
| type: BeaconHTTP | |
| config: | |
| host_bind: "0.0.0.0" | |
| port_bind: 8080 | |
| callback_addresses: | |
| - "$CALLBACK_HOST:8080" | |
| http_method: POST | |
| uri: | |
| - /beacon | |
| user_agent: | |
| - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" | |
| hb_header: "X-Beacon-Id" | |
| encrypt_key: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" | |
| ssl: false | |
| page-payload: '{"status":"ok","data":"<<<PAYLOAD_DATA>>>","metrics":"sync"}' | |
| agent: | |
| agent: beacon | |
| listener: ci_http | |
| listener_type: BeaconHTTP | |
| config: | |
| arch: x64 | |
| format: Exe | |
| sleep: "0s" | |
| jitter: 0 | |
| ssh: | |
| host: "$SSH_HOST" | |
| username: "$CI_USER" | |
| key_path: /root/.ssh/ci_key | |
| source_path: /tmp/ci_agent.exe | |
| agent_path: '$CI_AGENT_PATH' | |
| terminate: true | |
| preamble: | |
| - 'New-Item -ItemType Directory -Force -Path C:\ci | Out-Null' | |
| - 'Set-MpPreference -DisableRealtimeMonitoring \$true' | |
| - 'Add-MpPreference -ExclusionPath C:\ci' | |
| - 'New-NetFirewallRule -DisplayName CI_C2_8080 -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow -Profile Any | Out-Null' | |
| EOF | |
| # ── WSL: Pull images ────────────────────────────────────────────────────── | |
| - name: Pull CI images | |
| shell: wsl-bash {0} | |
| run: | | |
| sudo docker pull ghcr.io/tgjls/adaptixc2:latest | |
| sudo docker pull ghcr.io/tgjls/extension-kit:latest | |
| sudo docker pull ghcr.io/tgjls/testing-kit:latest | |
| # ── WSL: Build Extension-Kit ────────────────────────────────────────────── | |
| - name: Clone Extension-Kit source | |
| shell: wsl-bash {0} | |
| run: | | |
| git clone --depth 1 https://github.com/Adaptix-Framework/Extension-Kit /tmp/ext-src | |
| cd /tmp/ext-src | |
| git checkout dev | |
| # PR #139 fixes compile errors; skip if already merged into dev | |
| git fetch origin pull/139/head:pr-139 && git merge --no-edit pr-139 || \ | |
| echo "PR #139 merge skipped (likely already in dev)" | |
| - name: Build Extension-Kit via builder image | |
| shell: wsl-bash {0} | |
| run: | | |
| sudo docker run --rm \ | |
| -v /tmp/ext-src:/src \ | |
| ghcr.io/tgjls/extension-kit:latest | |
| # ── WSL: Prepare AdaptixC2 profile ──────────────────────────────────────── | |
| - name: Extract and patch profile.yaml to enable Extension-Kit | |
| shell: wsl-bash {0} | |
| run: | | |
| # Extract profile without running the ENTRYPOINT | |
| CID=$(sudo docker create ghcr.io/tgjls/adaptixc2:latest) | |
| sudo docker cp "$CID":/app/profile.yaml /tmp/ci_profile.yaml | |
| sudo docker rm "$CID" | |
| # Uncomment the Extension-Kit axscript line | |
| sed -i 's|# - "Extension-Kit/extension-kit.axs"| - "Extension-Kit/extension-kit.axs"|' /tmp/ci_profile.yaml | |
| # ── WSL: Start AdaptixC2 server ─────────────────────────────────────────── | |
| - name: Start AdaptixC2 server | |
| shell: wsl-bash {0} | |
| run: | | |
| sudo docker run -d --name adaptixc2 \ | |
| --network host \ | |
| -v /tmp/ext-src:/app/Extension-Kit:ro \ | |
| -v /tmp/ci_profile.yaml:/app/profile.yaml:ro \ | |
| ghcr.io/tgjls/adaptixc2:latest | |
| echo "Waiting for AdaptixC2 (port 4321)..." | |
| for i in $(seq 1 60); do | |
| (exec 3<>/dev/tcp/127.0.0.1/4321) 2>/dev/null && { echo "Server ready."; break; } | |
| sleep 1 | |
| done | |
| (exec 3<>/dev/tcp/127.0.0.1/4321) 2>/dev/null || { | |
| echo "=== AdaptixC2 server failed to start within 60s ===" | |
| sudo docker logs adaptixc2 | |
| exit 1 | |
| } | |
| # ── WSL: Tests ──────────────────────────────────────────────────────────── | |
| - name: Feature validation | |
| shell: wsl-bash {0} | |
| run: | | |
| sudo docker run --rm \ | |
| ghcr.io/tgjls/testing-kit:latest --help 2>&1 | \ | |
| grep -q -- "-o" && echo "✓ --output flag available" || \ | |
| { echo "✗ --output flag missing"; exit 1; } | |
| - name: Integration tests | |
| shell: wsl-bash {0} | |
| run: | | |
| WIN_WS=$(cmd.exe /c "echo %GITHUB_WORKSPACE%" 2>/dev/null | tr -d '\r') | |
| WSL_WS=$(wslpath "$WIN_WS") | |
| sudo docker run --rm \ | |
| --network host \ | |
| -v "$WSL_WS/.github/ci/tasks.yaml":/tasks.yaml:ro \ | |
| -v /tmp/ci_config.yaml:/tmp/ci_config.yaml:ro \ | |
| -v ~/.ssh/ci_key:/root/.ssh/ci_key:ro \ | |
| ghcr.io/tgjls/testing-kit:latest \ | |
| -c /tmp/ci_config.yaml -t /tasks.yaml | |
| # ── Cleanup ─────────────────────────────────────────────────────────────── | |
| - name: Print AdaptixC2 server logs | |
| if: always() | |
| shell: wsl-bash {0} | |
| run: sudo docker logs adaptixc2 2>&1 || true | |
| - name: Teardown | |
| if: always() | |
| shell: wsl-bash {0} | |
| run: | | |
| sudo docker rm -f adaptixc2 2>/dev/null || true | |
| rm -f ~/.ssh/ci_key ~/.ssh/ci_key.pub |