From bf20a84a9495974b6bfa9b637ca0d33e64d6f3c4 Mon Sep 17 00:00:00 2001 From: Ajay Krishnan Date: Mon, 20 Jul 2026 00:27:41 -0700 Subject: [PATCH] Use private host transport in containerized CI --- .github/workflows/candidate.yml | 7 +++++++ scripts/run_image_contract.sh | 17 +++++++++++++++-- test/image_contract_evidence_test.rb | 17 +++++++++++++++++ test/repository_test.rb | 6 ++++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/.github/workflows/candidate.yml b/.github/workflows/candidate.yml index f6d5b10..7489716 100644 --- a/.github/workflows/candidate.yml +++ b/.github/workflows/candidate.yml @@ -298,6 +298,13 @@ jobs: actual_client_sha="$(git -C ruby-client rev-parse HEAD)" test "$actual_client_sha" = "$EXPECTED_CLIENT_SHA" echo "value=$actual_client_sha" >> "$GITHUB_OUTPUT" + - name: Resolve the private Docker host transport + run: | + probe_host="$(ip -4 route show default | awk '/^default / {print $3; exit}')" + ruby -ripaddr -e \ + 'address = IPAddr.new(ARGV.fetch(0)); exit(address.ipv4? && address.private? ? 0 : 1)' \ + "$probe_host" + echo "OPENCODE_PROBE_HOST=$probe_host" >> "$GITHUB_ENV" - name: Exercise the complete manifest matrix without artifact claims env: BUNDLE_GEMFILE: ${{ github.workspace }}/ruby-client/Gemfile diff --git a/scripts/run_image_contract.sh b/scripts/run_image_contract.sh index d36506d..1eeed3f 100755 --- a/scripts/run_image_contract.sh +++ b/scripts/run_image_contract.sh @@ -6,6 +6,7 @@ image="${OPENCODE_IMAGE:-}" gem_path="${OPENCODE_RUBY_PATH:-}" expected_gem_commit="${OPENCODE_RUBY_COMMIT:-}" evidence_path="${OPENCODE_COMPAT_EVIDENCE_PATH:-}" +probe_host="${OPENCODE_PROBE_HOST:-127.0.0.1}" container_name="opencode-compat-${RANDOM}-$$" llm_container_name="opencode-compat-llm-${RANDOM}-$$" network_name="opencode-compat-net-${RANDOM}-$$" @@ -97,6 +98,18 @@ if [[ ! "$image" =~ ^[^[:space:]@]+@sha256:[0-9a-f]{64}$ ]]; then exit 2 fi fi +if ! ruby -ripaddr -e ' + begin + address = IPAddr.new(ARGV.fetch(0)) + valid = address.ipv4? && (address.loopback? || address.private?) + rescue IPAddr::InvalidAddressError + valid = false + end + exit(valid ? 0 : 1) +' "$probe_host"; then + echo "OPENCODE_PROBE_HOST must be a loopback or private IPv4 address" >&2 + exit 2 +fi if [[ ! "$expected_gem_commit" =~ ^[0-9a-f]{40}$ ]]; then echo "OPENCODE_RUBY_COMMIT must be a full lowercase 40-character Git commit" >&2 @@ -171,7 +184,7 @@ config_json="$(jq -cn --arg url "http://compat-llm:8080/v1" '{ docker run --detach \ --name "$container_name" \ --network "$network_name" \ - --publish 127.0.0.1::4096 \ + --publish "${probe_host}::4096" \ --env "OPENCODE_CONFIG_CONTENT=$config_json" \ --env OPENCODE_DISABLE_AUTOUPDATE=1 \ --env OPENCODE_DISABLE_AUTOCOMPACT=1 \ @@ -183,7 +196,7 @@ docker run --detach \ opencode_container_started=1 host_port="$(docker port "$container_name" 4096/tcp | sed -E 's/.*:([0-9]+)$/\1/' | head -1)" -base_url="http://127.0.0.1:${host_port}" +base_url="http://${probe_host}:${host_port}" ready=0 for _ in $(seq 1 120); do diff --git a/test/image_contract_evidence_test.rb b/test/image_contract_evidence_test.rb index 6ba5fce..9f1ca78 100644 --- a/test/image_contract_evidence_test.rb +++ b/test/image_contract_evidence_test.rb @@ -108,6 +108,23 @@ class ImageContractEvidenceTest < Minitest::Test assert_failed_without_docker(evidence_path) end + def test_rejects_malformed_ipv6_and_public_probe_hosts_before_docker + checkout, commit = git_checkout + + ["not-an-address", "::1", "8.8.8.8"].each_with_index do |probe_host, index| + evidence_path = File.join(@tmp, "probe-host-#{index}.json") + environment = base_environment(checkout, commit, evidence_path).merge( + "OPENCODE_PROBE_HOST" => probe_host + ) + + _output, error, status = run_contract(environment) + + assert_equal 2, status.exitstatus + assert_match(/loopback or private IPv4 address/, error) + assert_failed_without_docker(evidence_path) + end + end + private def base_environment(checkout, commit, evidence_path) diff --git a/test/repository_test.rb b/test/repository_test.rb index 8203f09..007dbf2 100644 --- a/test/repository_test.rb +++ b/test/repository_test.rb @@ -287,6 +287,9 @@ class RepositoryTest < Minitest::Test assert_includes workflow, "exact-image-contract-gitea:" assert_includes workflow, "if: github.server_url != 'https://github.com'" assert_includes workflow, "run: scripts/run_image_matrix_contract.sh" + assert_includes workflow, "ip -4 route show default" + assert_includes workflow, "address.ipv4? && address.private?" + assert_includes workflow, 'echo "OPENCODE_PROBE_HOST=$probe_host" >> "$GITHUB_ENV"' assert_operator workflow.scan('BUNDLE_GEMFILE: ${{ github.workspace }}/ruby-client/Gemfile').length, :>=, 2 assert_operator workflow.scan("generated JSON is transient and is not review evidence").length, :>=, 3 @@ -327,6 +330,9 @@ class RepositoryTest < Minitest::Test assert_includes runner, "exact_live_contract.rb" assert_includes runner, 'docker cp "$repo_root/scripts/fake_llm.py"' refute_includes runner, '--volume "$repo_root/scripts:/compat:ro"' + assert_includes runner, '--publish "${probe_host}::4096"' + assert_includes runner, 'base_url="http://${probe_host}:${host_port}"' + refute_includes runner, "--publish 0.0.0.0" assert_includes runner, "OPENCODE_COMPAT_EVIDENCE_PATH" assert_includes runner, "OPENCODE_EXPECTED_VERSION" refute_match(/request_count.*-lt\s+1/, runner)