name: Reusable Container on: workflow_call: inputs: ref: description: Exact commit SHA to build and validate required: true type: string permissions: contents: read security-events: write jobs: container: runs-on: ubuntu-latest timeout-minutes: 45 env: COMPOSE_PROJECT_NAME: cpr-ci-${{ github.run_id }}-${{ github.run_attempt }} CPR_IMAGE: codex-proxy-rs:ci-${{ inputs.ref }} TRIVY_QUIET: 'true' TRIVY_SKIP_VERSION_CHECK: 'true' steps: - name: Define isolated runtime values shell: bash run: | set -euo pipefail admin_password="$(openssl rand -hex 24)" postgres_password="$(openssl rand -hex 24)" redis_password="$(openssl rand -hex 24)" echo "::add-mask::$admin_password" echo "::add-mask::$postgres_password" echo "::add-mask::$redis_password" { echo "DIAGNOSTICS_DIR=$RUNNER_TEMP/codex-proxy-rs-diagnostics" echo "CI_ADMIN_PASSWORD=$admin_password" echo "CI_POSTGRES_PASSWORD=$postgres_password" echo "CI_REDIS_PASSWORD=$redis_password" } >> "$GITHUB_ENV" - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ inputs.ref }} - name: Setup Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Build runtime image uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . file: deploy/Dockerfile target: runtime load: true tags: ${{ env.CPR_IMAGE }} cache-from: type=gha,scope=runtime-ci cache-to: type=gha,mode=max,scope=runtime-ci - name: Generate Trivy SARIF uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.CPR_IMAGE }} version: v0.72.0 format: sarif output: trivy-results.sarif severity: HIGH,CRITICAL vuln-type: os,library scanners: vuln ignore-unfixed: true exit-code: '0' - name: Upload Trivy SARIF if: >- ${{ hashFiles('trivy-results.sarif') != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 with: sarif_file: trivy-results.sarif category: runtime-container - name: Enforce Trivy vulnerability gate uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.CPR_IMAGE }} version: v0.72.0 format: table severity: HIGH,CRITICAL vuln-type: os,library scanners: vuln ignore-unfixed: true exit-code: '1' - name: Prepare isolated runtime shell: bash run: | set -euo pipefail umask 0077 # Bind roots hand off ownership to the image-specific database users. # Sticky directories keep that initialization compatible with user-namespaced runners. install -d -m 1777 .runtime/postgres .runtime/redis mkdir -p "$DIAGNOSTICS_DIR" sudo install -d -m 0770 -o "$(id -u)" -g 10001 \ .runtime/data .runtime/logs ruby <<'RUBY' path = "deploy/config.example.yaml" content = File.read(path) replacements = { "password: &postgres_password ''" => "password: &postgres_password '#{ENV.fetch("CI_POSTGRES_PASSWORD")}'", "password: &redis_password ''" => "password: &redis_password '#{ENV.fetch("CI_REDIS_PASSWORD")}'", "default_password: ''" => "default_password: '#{ENV.fetch("CI_ADMIN_PASSWORD")}'", } replacements.each do |placeholder, configured| raise "missing config placeholder: #{placeholder}" unless content.sub!(placeholder, configured) end File.write("deploy/config.yaml", content) RUBY sudo chown "$(id -u):10001" deploy/config.yaml chmod 0640 deploy/config.yaml - name: Validate Docker Compose shell: bash run: | set -euo pipefail docker compose --project-name "$COMPOSE_PROJECT_NAME" \ -f deploy/compose.yaml config --quiet - name: Configure Redis host memory overcommit shell: bash run: | set -euo pipefail sudo sysctl -w vm.overcommit_memory=1 test "$(sysctl -n vm.overcommit_memory)" = 1 - name: Verify PostgreSQL and Redis volume persistence shell: bash run: | set -euo pipefail compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \ -f deploy/compose.yaml) "${compose[@]}" up -d --no-build --wait postgres redis test "$("${compose[@]}" exec -T -e PGPASSWORD="$CI_POSTGRES_PASSWORD" postgres \ psql -At -U codex_proxy -d codex_proxy -c "SHOW TimeZone;" | tr -d '\r')" = Asia/Shanghai "${compose[@]}" exec -T -e PGPASSWORD="$CI_POSTGRES_PASSWORD" postgres \ psql -v ON_ERROR_STOP=1 -U codex_proxy -d codex_proxy -c \ "CREATE TABLE cpr_ci_persistence_probe (value text PRIMARY KEY); INSERT INTO cpr_ci_persistence_probe VALUES ('persisted');" "${compose[@]}" exec -T -e REDISCLI_AUTH="$CI_REDIS_PASSWORD" redis \ redis-cli SET cpr:ci:persistence-probe persisted "${compose[@]}" up -d --no-build --wait --force-recreate postgres redis test "$("${compose[@]}" exec -T -e PGPASSWORD="$CI_POSTGRES_PASSWORD" postgres \ psql -At -U codex_proxy -d codex_proxy -c \ "SELECT value FROM cpr_ci_persistence_probe;")" = persisted test "$("${compose[@]}" exec -T -e REDISCLI_AUTH="$CI_REDIS_PASSWORD" redis \ redis-cli --raw GET cpr:ci:persistence-probe | tr -d '\r')" = persisted "${compose[@]}" exec -T -e PGPASSWORD="$CI_POSTGRES_PASSWORD" postgres \ psql -v ON_ERROR_STOP=1 -U codex_proxy -d codex_proxy -c \ "DROP TABLE cpr_ci_persistence_probe;" "${compose[@]}" exec -T -e REDISCLI_AUTH="$CI_REDIS_PASSWORD" redis \ redis-cli DEL cpr:ci:persistence-probe - name: Start and verify application shell: bash run: | set -euo pipefail compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \ -f deploy/compose.yaml) "${compose[@]}" up -d --no-build --wait codex-proxy-rs status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ "http://127.0.0.1:8080/healthz")" test "$status" = 204 test "$("${compose[@]}" exec -T codex-proxy-rs id -u | tr -d '\r')" = 10001 test "$("${compose[@]}" exec -T codex-proxy-rs id -g | tr -d '\r')" = 10001 test "$("${compose[@]}" exec -T codex-proxy-rs date +%z | tr -d '\r')" = +0800 test "$("${compose[@]}" exec -T codex-proxy-rs cat /etc/timezone | tr -d '\r')" = Asia/Shanghai "${compose[@]}" exec -T codex-proxy-rs sh -c \ 'touch /app/.runtime/data/.ci-write /app/.runtime/logs/.ci-write && rm /app/.runtime/data/.ci-write /app/.runtime/logs/.ci-write' - name: Verify clean logs and graceful shutdown shell: bash run: | set -euo pipefail compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \ -f deploy/compose.yaml) "${compose[@]}" stop --timeout 30 codex-proxy-rs container_id="$("${compose[@]}" ps -a -q codex-proxy-rs)" test -n "$container_id" test "$(docker inspect --format '{{.State.ExitCode}}' "$container_id")" = 0 "${compose[@]}" logs --no-color --timestamps codex-proxy-rs \ > "$DIAGNOSTICS_DIR/application.log" "${compose[@]}" logs --no-color --timestamps postgres \ > "$DIAGNOSTICS_DIR/postgres.log" "${compose[@]}" logs --no-color --timestamps redis \ > "$DIAGNOSTICS_DIR/redis.log" if grep --extended-regexp --ignore-case \ '"level"[[:space:]]*:[[:space:]]*"(warn|error|fatal|panic)"|(^|[^[:alnum:]_])panic([^[:alnum:]_]|$)' \ "$DIAGNOSTICS_DIR/application.log" \ || grep --extended-regexp --ignore-case \ '(^|[[:space:]])(warning|error|fatal|panic):' \ "$DIAGNOSTICS_DIR/postgres.log" \ || grep --extended-regexp --ignore-case \ '[0-9]+:[A-Z][[:space:]].*[[:space:]]#[[:space:]]|(^|[|[:space:]])(warning|error|fatal|panic)(:|[[:space:]])' \ "$DIAGNOSTICS_DIR/redis.log"; then echo "container logs contain a warning or error severity" >&2 exit 1 fi - name: Collect container diagnostics if: always() shell: bash run: | set +e mkdir -p "$DIAGNOSTICS_DIR" raw_dir="$(mktemp -d)" compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \ -f deploy/compose.yaml) "${compose[@]}" config > "$raw_dir/compose-config.yaml" 2>&1 "${compose[@]}" ps -a > "$raw_dir/compose-ps.txt" 2>&1 "${compose[@]}" logs --no-color --timestamps > "$raw_dir/compose.log" 2>&1 mapfile -t container_ids < <("${compose[@]}" ps -a -q) if [ "${#container_ids[@]}" -gt 0 ]; then docker inspect "${container_ids[@]}" > "$raw_dir/docker-inspect.json" 2>&1 fi RAW_DIR="$raw_dir" OUTPUT_DIR="$DIAGNOSTICS_DIR" ruby <<'RUBY' raw_dir = ENV.fetch("RAW_DIR") output_dir = ENV.fetch("OUTPUT_DIR") secrets = [ ENV.fetch("CI_ADMIN_PASSWORD"), ENV.fetch("CI_POSTGRES_PASSWORD"), ENV.fetch("CI_REDIS_PASSWORD"), ] encoded_secrets = secrets.map do |secret| secret.encode("UTF-8").bytes.map do |byte| character = byte.chr character.match?(/[A-Za-z0-9]/) ? character : format("%%%02X", byte) end.join end Dir.children(raw_dir).each do |name| content = File.binread(File.join(raw_dir, name)) (secrets + encoded_secrets).each do |secret| content = content.gsub(secret, "") end File.binwrite(File.join(output_dir, name), content) end RUBY rm -rf "$raw_dir" cp trivy-results.sarif "$DIAGNOSTICS_DIR/" 2>/dev/null || true - name: Upload container diagnostics if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: container-diagnostics-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/codex-proxy-rs-diagnostics if-no-files-found: warn retention-days: 7 - name: Remove isolated runtime if: always() shell: bash run: | docker compose --project-name "$COMPOSE_PROJECT_NAME" \ -f deploy/compose.yaml down --remove-orphans || true sudo rm -rf "$GITHUB_WORKSPACE/.runtime" "$GITHUB_WORKSPACE/deploy/config.yaml"