Files
crank/.gitea/workflows/ci.yml
T
bsodfather 0e8f1ca03a
CI / Rust Checks (push) Failing after 4m28s
CI / UI Checks (push) Has been skipped
CI / Frontend E2E (push) Has been skipped
CI / Community Image Smoke (push) Has been skipped
CI / Deploy (push) Has been skipped
наблюдаемость: завершить базовый контур Community
Добавить структурированные журналы, метрики, трассировку и безопасный канал критических ошибок. Усилить границы рантайма, тесты, проверку зависимостей и сценарии развёртывания.
2026-07-31 01:01:14 +03:00

500 lines
20 KiB
YAML

name: CI
on:
pull_request:
push:
branches:
- main
- "feat/**"
env:
CARGO_BUILD_JOBS: "2"
CARGO_INCREMENTAL: "0"
RUST_TEST_THREADS: "2"
TESTCONTAINERS_RYUK_DISABLED: "true"
jobs:
rust:
name: Rust Checks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install Rust toolchain
run: |
set -eu
toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n1)"
if [ -z "$toolchain" ]; then
echo "Unable to read Rust toolchain channel from rust-toolchain.toml" >&2
exit 1
fi
rustup toolchain install "$toolchain" --profile minimal --component clippy --component rustfmt
rustup default "$toolchain"
host="$(rustc -vV | sed -n 's/^host: //p')"
toolchain_dir="${RUSTUP_HOME:-$HOME/.rustup}/toolchains/${toolchain}-${host}"
toolchain_bin="$toolchain_dir/bin"
if [ ! -x "$toolchain_bin/rustc" ] || [ ! -x "$toolchain_bin/cargo" ]; then
echo "Rust $toolchain was not installed at $toolchain_dir." >&2
exit 1
fi
printf '%s\n' "$toolchain_bin" >> "$GITHUB_PATH"
"$toolchain_bin/rustc" --version
"$toolchain_bin/cargo" --version
"$toolchain_bin/rustfmt" --version
"$toolchain_bin/cargo-clippy" --version
- name: Verify runner toolchain
run: |
python3 --version
rustc --version
cargo --version
rustfmt --version
cargo clippy --version
docker --version
docker info
- name: Install dependency policy tool
run: cargo install cargo-deny --version 0.20.2 --locked
- name: Run tooling unit tests
run: python3 -m unittest discover -s tests/unit
- name: Check Community scope
run: scripts/check-community-scope.sh
- name: Check formatting
run: cargo fmt --all --check
- name: Check Rust code health
run: scripts/check-rust-code-health.sh
- name: Check dependency licenses and advisories
run: cargo deny --locked check advisories bans licenses sources
- name: Check Rust boundaries
run: scripts/check-rust-boundaries.sh
- name: Run clippy
run: cargo clippy --workspace --all-targets --all-features --jobs "$CARGO_BUILD_JOBS" -- -D warnings
- name: Run tests
run: cargo test --workspace --all-targets --jobs "$CARGO_BUILD_JOBS"
ui:
name: UI Checks
runs-on: ubuntu-latest
needs: rust
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Verify runner toolchain
run: |
node --version
npm --version
docker --version
- name: Install UI dependencies
working-directory: apps/ui
run: npm ci
- name: Audit UI dependencies
working-directory: apps/ui
run: npm audit --audit-level=high
- name: Build UI bundle
working-directory: apps/ui
run: npm run build
frontend-e2e:
name: Frontend E2E
runs-on: ubuntu-latest
needs: ui
services:
crank-e2e-postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: crank
POSTGRES_USER: crank
POSTGRES_PASSWORD: crank
options: >-
--health-cmd "pg_isready -U crank -d crank"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
CRANK_E2E_USE_EXTERNAL_POSTGRES: "1"
CRANK_E2E_POSTGRES_HOST: crank-e2e-postgres
CRANK_E2E_POSTGRES_PORT: "5432"
CRANK_E2E_POSTGRES_DB: crank
CRANK_E2E_POSTGRES_USER: crank
CRANK_E2E_POSTGRES_PASSWORD: crank
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install Rust toolchain
run: |
set -eu
toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n1)"
if [ -z "$toolchain" ]; then
echo "Unable to read Rust toolchain channel from rust-toolchain.toml" >&2
exit 1
fi
rustup toolchain install "$toolchain" --profile minimal --component clippy --component rustfmt
rustup default "$toolchain"
host="$(rustc -vV | sed -n 's/^host: //p')"
toolchain_dir="${RUSTUP_HOME:-$HOME/.rustup}/toolchains/${toolchain}-${host}"
toolchain_bin="$toolchain_dir/bin"
if [ ! -x "$toolchain_bin/rustc" ] || [ ! -x "$toolchain_bin/cargo" ]; then
echo "Rust $toolchain was not installed at $toolchain_dir." >&2
exit 1
fi
printf '%s\n' "$toolchain_bin" >> "$GITHUB_PATH"
"$toolchain_bin/rustc" --version
"$toolchain_bin/cargo" --version
"$toolchain_bin/rustfmt" --version
"$toolchain_bin/cargo-clippy" --version
- name: Verify runner toolchain
run: |
rustc --version
cargo --version
node --version
npm --version
docker --version
- name: Install UI dependencies
working-directory: apps/ui
run: npm ci
- name: Install Playwright browser
working-directory: apps/ui
run: npx playwright install --with-deps chromium
- name: Prebuild e2e services
run: cargo build -p admin-api -p mcp-server --jobs "$CARGO_BUILD_JOBS"
- name: Run Playwright e2e
working-directory: apps/ui
run: npx playwright test
- name: Show Playwright stack logs
if: failure()
run: |
find .tmp/ui-e2e/logs -maxdepth 1 -type f -print -exec sed -n '1,220p' {} \; || true
deployment:
name: Community Image Smoke
runs-on: ubuntu-latest
needs:
- rust
- ui
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Validate Community deployment manifest
run: docker compose -f deploy/community/docker-compose.yml --env-file deploy/community/.env.example config -q
- name: Build Community images
run: |
docker build -f apps/admin-api/Dockerfile -t crank/admin-api:ci .
docker build -f apps/mcp-server/Dockerfile -t crank/mcp-server:ci .
docker build -f apps/ui/Dockerfile -t crank/ui:ci .
- name: Start Community image stack
run: |
mkdir -p .tmp
cat > .tmp/community-smoke.env <<'EOF'
COMPOSE_PROJECT_NAME=crank-ci-smoke
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=crank
POSTGRES_USER=crank
POSTGRES_PASSWORD=crank-ci-password
CRANK_ADMIN_API_IMAGE=crank/admin-api:ci
CRANK_MCP_SERVER_IMAGE=crank/mcp-server:ci
CRANK_UI_IMAGE=crank/ui:ci
CRANK_MASTER_KEY=0000000000000000000000000000000000000000000000000000000000000000
CRANK_SESSION_SECRET=ci-session-secret
CRANK_PASSWORD_PEPPER=ci-password-pepper
CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.test
CRANK_BOOTSTRAP_ADMIN_PASSWORD=ci-admin-password
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME=CI Owner
CRANK_BASE_URL=http://127.0.0.1:3000
CRANK_PUBLISH_BIND=127.0.0.1
CRANK_DEMO_SEED=true
EOF
docker compose -f deploy/community/docker-compose.images.yml \
--env-file .tmp/community-smoke.env --profile local-db up -d --wait
- name: Run authenticated Community image smoke
env:
CRANK_STAGING_ADMIN_EMAIL: owner@crank.test
CRANK_STAGING_ADMIN_PASSWORD: ci-admin-password
run: scripts/authenticated-product-smoke.sh http://127.0.0.1:3000
- name: Show Community image logs
if: failure()
run: |
docker compose -f deploy/community/docker-compose.images.yml \
--env-file .tmp/community-smoke.env --profile local-db ps || true
docker compose -f deploy/community/docker-compose.images.yml \
--env-file .tmp/community-smoke.env --profile local-db logs --no-color || true
- name: Stop Community image stack
if: always()
run: |
docker compose -f deploy/community/docker-compose.images.yml \
--env-file .tmp/community-smoke.env --profile local-db down -v --remove-orphans || true
deploy:
name: Deploy
runs-on: ubuntu-latest
needs:
- rust
- ui
- frontend-e2e
- deployment
if: ${{ gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' }}
env:
REGISTRY: git.itexp.me
IMAGE_TAG: ${{ gitea.sha }}
ADMIN_API_IMAGE: git.itexp.me/bsodfather/crank-community-admin-api
MCP_SERVER_IMAGE: git.itexp.me/bsodfather/crank-community-mcp-server
UI_IMAGE: git.itexp.me/bsodfather/crank-community-ui
OPENBAO_ENV_FILE: .openbao-env
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Verify runner toolchain
run: |
docker --version
command -v bao
bao version
- name: Load deployment secrets from OpenBao
env:
BAO_ADDR: ${{ secrets.BAO_ADDR }}
BAO_ROLE_ID: ${{ secrets.BAO_ROLE_ID }}
BAO_SECRET_ID: ${{ secrets.BAO_SECRET_ID }}
OPENBAO_APP: crank
run: scripts/load-openbao-env.sh
- name: Login to registry
run: |
. "$OPENBAO_ENV_FILE"
printf '%s' "$DEPLOY_REGISTRY_TOKEN" | \
docker login '${{ env.REGISTRY }}' -u "$DEPLOY_REGISTRY_USER" --password-stdin
- name: Build and push images
run: |
docker build -f apps/admin-api/Dockerfile \
-t '${{ env.ADMIN_API_IMAGE }}:${{ env.IMAGE_TAG }}' \
-t '${{ env.ADMIN_API_IMAGE }}:main' \
.
docker build -f apps/mcp-server/Dockerfile \
-t '${{ env.MCP_SERVER_IMAGE }}:${{ env.IMAGE_TAG }}' \
-t '${{ env.MCP_SERVER_IMAGE }}:main' \
.
docker build -f apps/ui/Dockerfile \
-t '${{ env.UI_IMAGE }}:${{ env.IMAGE_TAG }}' \
-t '${{ env.UI_IMAGE }}:main' \
.
scripts/scan-images.sh \
'${{ env.ADMIN_API_IMAGE }}:${{ env.IMAGE_TAG }}' \
'${{ env.MCP_SERVER_IMAGE }}:${{ env.IMAGE_TAG }}' \
'${{ env.UI_IMAGE }}:${{ env.IMAGE_TAG }}'
docker push '${{ env.ADMIN_API_IMAGE }}:${{ env.IMAGE_TAG }}'
docker push '${{ env.ADMIN_API_IMAGE }}:main'
docker push '${{ env.MCP_SERVER_IMAGE }}:${{ env.IMAGE_TAG }}'
docker push '${{ env.MCP_SERVER_IMAGE }}:main'
docker push '${{ env.UI_IMAGE }}:${{ env.IMAGE_TAG }}'
docker push '${{ env.UI_IMAGE }}:main'
- name: Configure SSH key
run: |
. "$OPENBAO_ENV_FILE"
mkdir -p ~/.ssh
chmod 700 ~/.ssh
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
- name: Configure known hosts
run: |
. "$OPENBAO_ENV_FILE"
if [ -n "${DEPLOY_KNOWN_HOSTS:-}" ]; then
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
else
ssh-keyscan -p "${DEPLOY_PORT:-22}" "$DEPLOY_HOST" > ~/.ssh/known_hosts
fi
chmod 644 ~/.ssh/known_hosts
- name: Sync deployment files to server
run: |
. "$OPENBAO_ENV_FILE"
ssh -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" \
"mkdir -p '$DEPLOY_PATH' && \
if [ -f '$DEPLOY_PATH/docker-compose.yml' ]; then \
cp '$DEPLOY_PATH/docker-compose.yml' '$DEPLOY_PATH/docker-compose.previous.yml'; \
fi"
rsync -az -e "ssh -p $DEPLOY_PORT" deploy/community/docker-compose.yml \
"$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/docker-compose.yml"
rsync -az -e "ssh -p $DEPLOY_PORT" scripts/deploy-community.sh \
"$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/deploy-community.sh"
- name: Write environment file
run: |
. "$OPENBAO_ENV_FILE"
tmp_env="$(mktemp)"
append_if_set() {
if [ -n "$2" ]; then
printf '%s=%s\n' "$1" "$2" >> "$tmp_env"
fi
}
: > "$tmp_env"
append_if_set POSTGRES_DB "$POSTGRES_DB"
append_if_set POSTGRES_USER "$POSTGRES_USER"
append_if_set POSTGRES_PASSWORD "$POSTGRES_PASSWORD"
append_if_set POSTGRES_HOST "$POSTGRES_HOST"
append_if_set POSTGRES_MAX_CONNECTIONS "${POSTGRES_MAX_CONNECTIONS:-}"
append_if_set POSTGRES_MIN_CONNECTIONS "${POSTGRES_MIN_CONNECTIONS:-}"
append_if_set POSTGRES_ACQUIRE_TIMEOUT_MS "${POSTGRES_ACQUIRE_TIMEOUT_MS:-}"
append_if_set POSTGRES_IDLE_TIMEOUT_MS "${POSTGRES_IDLE_TIMEOUT_MS:-}"
append_if_set POSTGRES_MAX_LIFETIME_MS "${POSTGRES_MAX_LIFETIME_MS:-}"
if [ -n "${POSTGRES_PORT:-}" ]; then
append_if_set POSTGRES_PORT "$POSTGRES_PORT"
elif [ -n "${PGBOUNCER_PORT:-}" ]; then
append_if_set POSTGRES_PORT "$PGBOUNCER_PORT"
fi
append_if_set CRANK_STORAGE_ROOT "$CRANK_STORAGE_ROOT"
append_if_set CRANK_PUBLISH_BIND "$CRANK_PUBLISH_BIND"
append_if_set CRANK_ADMIN_BIND "$CRANK_ADMIN_BIND"
append_if_set CRANK_MCP_BIND "$CRANK_MCP_BIND"
append_if_set CRANK_MCP_REFRESH_MS "$CRANK_MCP_REFRESH_MS"
append_if_set CRANK_ADMIN_RATE_LIMIT_RPS "${CRANK_ADMIN_RATE_LIMIT_RPS:-}"
append_if_set CRANK_ADMIN_RATE_LIMIT_BURST "${CRANK_ADMIN_RATE_LIMIT_BURST:-}"
append_if_set CRANK_MCP_RATE_LIMIT_RPS "${CRANK_MCP_RATE_LIMIT_RPS:-}"
append_if_set CRANK_MCP_RATE_LIMIT_BURST "${CRANK_MCP_RATE_LIMIT_BURST:-}"
append_if_set CRANK_RUNTIME_MAX_CONCURRENT_UNARY "${CRANK_RUNTIME_MAX_CONCURRENT_UNARY:-}"
append_if_set CRANK_RUNTIME_MAX_CONCURRENT_SESSIONS "${CRANK_RUNTIME_MAX_CONCURRENT_SESSIONS:-}"
append_if_set CRANK_OUTBOUND_ALLOWED_HOSTS "${CRANK_OUTBOUND_ALLOWED_HOSTS:-}"
append_if_set CRANK_OUTBOUND_DENIED_HOSTS "${CRANK_OUTBOUND_DENIED_HOSTS:-}"
append_if_set CRANK_OUTBOUND_MAX_RESPONSE_BYTES "${CRANK_OUTBOUND_MAX_RESPONSE_BYTES:-}"
append_if_set CRANK_ENVIRONMENT "${CRANK_ENVIRONMENT:-production}"
append_if_set CRANK_LOG_LEVEL "$CRANK_LOG_LEVEL"
append_if_set CRANK_SENTRY_DSN "${CRANK_SENTRY_DSN:-}"
append_if_set CRANK_METRICS_ENABLED "${CRANK_METRICS_ENABLED:-}"
append_if_set CRANK_ADMIN_METRICS_BIND "${CRANK_ADMIN_METRICS_BIND:-}"
append_if_set CRANK_MCP_METRICS_BIND "${CRANK_MCP_METRICS_BIND:-}"
append_if_set CRANK_METRICS_BEARER_TOKEN "${CRANK_METRICS_BEARER_TOKEN:-}"
append_if_set CRANK_INVOCATION_LOG_RETENTION_DAYS "${CRANK_INVOCATION_LOG_RETENTION_DAYS:-}"
append_if_set OTEL_EXPORTER_OTLP_ENDPOINT "${OTEL_EXPORTER_OTLP_ENDPOINT:-}"
append_if_set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT "${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-}"
append_if_set OTEL_EXPORTER_OTLP_PROTOCOL "${OTEL_EXPORTER_OTLP_PROTOCOL:-}"
append_if_set OTEL_EXPORTER_OTLP_TRACES_PROTOCOL "${OTEL_EXPORTER_OTLP_TRACES_PROTOCOL:-}"
append_if_set OTEL_EXPORTER_OTLP_TIMEOUT "${OTEL_EXPORTER_OTLP_TIMEOUT:-}"
append_if_set OTEL_EXPORTER_OTLP_TRACES_TIMEOUT "${OTEL_EXPORTER_OTLP_TRACES_TIMEOUT:-}"
append_if_set OTEL_EXPORTER_OTLP_HEADERS "${OTEL_EXPORTER_OTLP_HEADERS:-}"
append_if_set OTEL_EXPORTER_OTLP_TRACES_HEADERS "${OTEL_EXPORTER_OTLP_TRACES_HEADERS:-}"
append_if_set OTEL_BSP_MAX_QUEUE_SIZE "${OTEL_BSP_MAX_QUEUE_SIZE:-}"
append_if_set OTEL_BSP_MAX_EXPORT_BATCH_SIZE "${OTEL_BSP_MAX_EXPORT_BATCH_SIZE:-}"
append_if_set OTEL_BSP_SCHEDULE_DELAY "${OTEL_BSP_SCHEDULE_DELAY:-}"
append_if_set OTEL_BSP_EXPORT_TIMEOUT "${OTEL_BSP_EXPORT_TIMEOUT:-}"
append_if_set CRANK_MASTER_KEY "$CRANK_MASTER_KEY"
append_if_set CRANK_BASE_URL "$CRANK_BASE_URL"
append_if_set CRANK_CACHE_BACKEND "$CRANK_CACHE_BACKEND"
append_if_set CRANK_CACHE_URL "$CRANK_CACHE_URL"
append_if_set CRANK_CACHE_DEFAULT_TTL_MS "$CRANK_CACHE_DEFAULT_TTL_MS"
append_if_set CRANK_SESSION_SECRET "$CRANK_SESSION_SECRET"
append_if_set CRANK_PASSWORD_PEPPER "$CRANK_PASSWORD_PEPPER"
append_if_set CRANK_SESSION_TTL_HOURS "$CRANK_SESSION_TTL_HOURS"
append_if_set CRANK_BOOTSTRAP_ADMIN_EMAIL "$CRANK_BOOTSTRAP_ADMIN_EMAIL"
append_if_set CRANK_BOOTSTRAP_ADMIN_PASSWORD "$CRANK_BOOTSTRAP_ADMIN_PASSWORD"
append_if_set CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME "$CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME"
append_if_set CRANK_DEMO_SEED "$CRANK_DEMO_SEED"
{
printf 'COMPOSE_PROJECT_NAME=community\n'
printf 'CRANK_ADMIN_API_IMAGE=%s:%s\n' '${{ env.ADMIN_API_IMAGE }}' '${{ env.IMAGE_TAG }}'
printf 'CRANK_MCP_SERVER_IMAGE=%s:%s\n' '${{ env.MCP_SERVER_IMAGE }}' '${{ env.IMAGE_TAG }}'
printf 'CRANK_UI_IMAGE=%s:%s\n' '${{ env.UI_IMAGE }}' '${{ env.IMAGE_TAG }}'
} >> "$tmp_env"
cat "$tmp_env" | ssh -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" \
"mkdir -p '$DEPLOY_PATH' && \
if [ -f '$DEPLOY_PATH/.env' ]; then \
cp '$DEPLOY_PATH/.env' '$DEPLOY_PATH/.env.previous'; \
fi && cat > '$DEPLOY_PATH/.env'"
rm -f "$tmp_env"
- name: Validate required environment variables
run: |
. "$OPENBAO_ENV_FILE"
ssh -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" "
set -e
cd '$DEPLOY_PATH'
required_vars='
POSTGRES_HOST
POSTGRES_PORT
POSTGRES_DB
POSTGRES_USER
POSTGRES_PASSWORD
CRANK_MASTER_KEY
CRANK_SESSION_SECRET
CRANK_PASSWORD_PEPPER
CRANK_BOOTSTRAP_ADMIN_EMAIL
CRANK_BOOTSTRAP_ADMIN_PASSWORD
CRANK_BASE_URL
'
for var in \$required_vars; do
value=\$(grep -E \"^\${var}=\" .env | tail -n1 | cut -d= -f2- || true)
if [ -z \"\$value\" ]; then
echo \"missing required env: \$var\" >&2
exit 1
fi
done
"
- name: Deploy with Docker Compose
run: |
. "$OPENBAO_ENV_FILE"
printf '%s' "$DEPLOY_REGISTRY_TOKEN" | ssh -p "$DEPLOY_PORT" \
"$DEPLOY_USER@$DEPLOY_HOST" \
"docker login '${{ env.REGISTRY }}' -u '$DEPLOY_REGISTRY_USER' --password-stdin"
ssh -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" \
"chmod 700 '$DEPLOY_PATH/deploy-community.sh' && \
'$DEPLOY_PATH/deploy-community.sh' '$DEPLOY_PATH'"
- name: Verify health endpoints
run: |
. "$OPENBAO_ENV_FILE"
ssh -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" "
set -e
cd '$DEPLOY_PATH'
for attempt in \$(seq 1 30); do
if curl --fail --silent http://127.0.0.1:3000/ >/dev/null \
&& curl --fail --silent http://127.0.0.1:3001/ready >/dev/null \
&& curl --fail --silent http://127.0.0.1:3002/ready >/dev/null; then
exit 0
fi
sleep 2
done
echo 'deployment health verification failed' >&2
docker compose ps >&2
exit 1
"
- name: Run authenticated product smoke
run: |
. "$OPENBAO_ENV_FILE"
CRANK_STAGING_ADMIN_EMAIL="$CRANK_BOOTSTRAP_ADMIN_EMAIL" \
CRANK_STAGING_ADMIN_PASSWORD="$CRANK_BOOTSTRAP_ADMIN_PASSWORD" \
scripts/authenticated-product-smoke.sh "$CRANK_BASE_URL"