mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
Share container verification between CI and release image jobs. Validate both native release architectures and publish manifests from the verified digests, eliminating the duplicate release source build. Persist BuildKit Cargo cache mounts across CI runs, refresh release caches per commit, and generate SARIF and vulnerability gate results from one Trivy scan. Validation: actionlint, composite action ShellCheck, embedded shell syntax, Trivy severity and malformed-report checks, and manifest digest validation passed locally. Full GitHub workflow timing remains to be measured. Co-authored-by: Codex <noreply@openai.com>
243 lines
10 KiB
YAML
243 lines
10 KiB
YAML
name: Verify container
|
|
description: Scan and validate a built image with the deployment Compose configuration
|
|
inputs:
|
|
image:
|
|
description: Local image tag or immutable registry digest to validate
|
|
required: true
|
|
platform:
|
|
description: Native runner image platform
|
|
required: true
|
|
artifact-name:
|
|
description: Unique diagnostics artifact name within the workflow run
|
|
required: true
|
|
runs:
|
|
using: composite
|
|
steps:
|
|
- name: Define isolated runtime values
|
|
shell: bash
|
|
env:
|
|
IMAGE: ${{ inputs.image }}
|
|
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 "CPR_IMAGE=$IMAGE"
|
|
echo "COMPOSE_PROJECT_NAME=cpr-check-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${RUNNER_ARCH,,}"
|
|
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: Scan container image
|
|
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
|
env:
|
|
TRIVY_PLATFORM: ${{ inputs.platform }}
|
|
TRIVY_QUIET: 'true'
|
|
TRIVY_SKIP_VERSION_CHECK: 'true'
|
|
with:
|
|
image-ref: ${{ inputs.image }}
|
|
version: v0.72.0
|
|
format: json
|
|
output: trivy-results.json
|
|
severity: HIGH,CRITICAL
|
|
vuln-type: os,library
|
|
scanners: vuln
|
|
ignore-unfixed: true
|
|
exit-code: '0'
|
|
|
|
- name: Generate Trivy SARIF
|
|
shell: bash
|
|
run: trivy convert --format sarif --output trivy-results.sarif trivy-results.json
|
|
|
|
- name: Upload Trivy SARIF
|
|
if: >-
|
|
${{ always() && 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@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
|
with:
|
|
sarif_file: trivy-results.sarif
|
|
category: runtime-container-${{ inputs.platform }}
|
|
|
|
- name: Enforce Trivy vulnerability gate
|
|
shell: bash
|
|
run: trivy convert --format table --severity HIGH,CRITICAL --exit-code 1 trivy-results.json
|
|
|
|
- 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, "<redacted>")
|
|
end
|
|
File.binwrite(File.join(output_dir, name), content)
|
|
end
|
|
RUBY
|
|
rm -rf "$raw_dir"
|
|
cp trivy-results.json 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: ${{ inputs.artifact-name }}
|
|
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"
|