From 7b5053f42a2ee87987fb7b47da059555688b9f17 Mon Sep 17 00:00:00 2001 From: Zuev Date: Wed, 29 Jul 2026 23:39:35 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D1=87=D1=83?= =?UTF-8?q?=20=D0=B8=D1=81=D1=85=D0=BE=D0=B4=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B2=20CI-=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD=D0=B5?= =?UTF-8?q?=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/docker-build.yaml | 59 ++++++++----- docs/INFRASTRUCTURE.md | 10 ++- ...u_specified_requires_a_project_to_execu.md | 25 ++++++ scripts/check-artifact-pinning.sh | 17 +++- scripts/run-ci-container.sh | 87 +++++++++++++++++++ scripts/test-artifact-pinning.sh | 19 +++- scripts/test-run-ci-container.sh | 75 ++++++++++++++++ 7 files changed, 264 insertions(+), 28 deletions(-) create mode 100644 graphify-out/memory/query_20260729_203853_the_goal_you_specified_requires_a_project_to_execu.md create mode 100755 scripts/run-ci-container.sh create mode 100755 scripts/test-run-ci-container.sh diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml index e1f9c06..ad8a099 100644 --- a/.gitea/workflows/docker-build.yaml +++ b/.gitea/workflows/docker-build.yaml @@ -25,21 +25,25 @@ jobs: - name: Получить исходный код uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Проверить запуск CI-контейнеров без bind mount + run: bash scripts/test-run-ci-container.sh + - name: Backend unit и component tests - run: >- - docker run --rm - -v "${{ github.workspace }}/backend:/workspace" - -w /workspace - "${{ env.MAVEN_IMAGE }}" - mvn -Dapi.version=1.44 '-Dtest=!*IntegrationTest' test + run: | + bash scripts/run-ci-container.sh \ + backend \ + "${{ env.MAVEN_IMAGE }}" \ + -- \ + mvn -Dapi.version=1.44 '-Dtest=!*IntegrationTest' test - name: Frontend static и unit tests - run: >- - docker run --rm - -v "${{ github.workspace }}/frontend:/workspace" - -w /workspace - "${{ env.NODE_IMAGE }}" - sh -c "npm ci && npm run check" + run: | + bash scripts/run-ci-container.sh \ + frontend \ + "${{ env.NODE_IMAGE }}" \ + --copy backend /backend \ + -- \ + sh -c "npm ci && npm run check" - name: Проверить Docker Compose env: @@ -146,6 +150,9 @@ jobs: BACKEND_REF: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}@${{ needs.build-and-push-backend.outputs.digest }} FRONTEND_REF: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}@${{ needs.build-and-push-frontend.outputs.digest }} steps: + - name: Получить исходный код + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Войти в Container Registry uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: @@ -154,20 +161,24 @@ jobs: password: ${{ secrets.ZUEV_TOKEN }} - name: Проверить backend на HIGH и CRITICAL уязвимости - run: >- - docker run --rm - -v "$HOME/.docker:/root/.docker:ro" - "${{ env.TRIVY_IMAGE }}" - image --scanners vuln --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed - "$BACKEND_REF" + run: | + bash scripts/run-ci-container.sh \ + "$HOME/.docker" \ + "${{ env.TRIVY_IMAGE }}" \ + --env DOCKER_CONFIG=/workspace \ + -- \ + image --scanners vuln --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \ + "$BACKEND_REF" - name: Проверить frontend на HIGH и CRITICAL уязвимости - run: >- - docker run --rm - -v "$HOME/.docker:/root/.docker:ro" - "${{ env.TRIVY_IMAGE }}" - image --scanners vuln --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed - "$FRONTEND_REF" + run: | + bash scripts/run-ci-container.sh \ + "$HOME/.docker" \ + "${{ env.TRIVY_IMAGE }}" \ + --env DOCKER_CONFIG=/workspace \ + -- \ + image --scanners vuln --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \ + "$FRONTEND_REF" deploy-to-k8s: name: Развернуть проверенные digests diff --git a/docs/INFRASTRUCTURE.md b/docs/INFRASTRUCTURE.md index c7e0a4a..9bf1986 100644 --- a/docs/INFRASTRUCTURE.md +++ b/docs/INFRASTRUCTURE.md @@ -361,13 +361,19 @@ Deployment-файлов запрещено. Основные шаги: 1. Backend unit/component tests, frontend static/unit tests, Compose validation, shell-тест - immutable rollout/rollback и регрессионная проверка закрепления артефактов. + immutable rollout/rollback и регрессионная проверка закрепления артефактов. Исходники + backend/frontend передаются тестовым контейнерам через `docker cp`: bind mount + `${{ github.workspace }}` не используется, потому что Docker daemon runner может + находиться вне job-контейнера и не видеть его файловую систему. Frontend security-тест + дополнительно получает backend в `/backend`, сохраняя ожидаемую соседнюю структуру. 2. Только после успешных gates — login, параллельная сборка и push backend/frontend с SHA-tag и release tag без `latest`. Сторонние Actions закреплены полными commit SHA. 3. BuildKit публикует для обоих образов максимальную provenance- и SBOM-attestation. Build jobs также возвращают registry digest каждого образа. 4. Отдельный обязательный job сканирует опубликованные digests закреплённым Trivy `0.63.0` и - блокирует deploy при исправимых `HIGH`/`CRITICAL` уязвимостях. + блокирует deploy при исправимых `HIGH`/`CRITICAL` уязвимостях. Docker credentials для + приватного registry копируются во временный Trivy-контейнер через Docker API; bind mount + домашнего каталога job-контейнера не используется. 5. Push в `main` выполняет проверки, сборку, публикацию и scan, но не меняет production. Deploy job запускается только вручную через `workflow_dispatch` и устанавливает фиксированный `kubectl v1.33.12` после SHA-256 проверки. Java Agent также имеет точную diff --git a/graphify-out/memory/query_20260729_203853_the_goal_you_specified_requires_a_project_to_execu.md b/graphify-out/memory/query_20260729_203853_the_goal_you_specified_requires_a_project_to_execu.md new file mode 100644 index 0000000..b22566a --- /dev/null +++ b/graphify-out/memory/query_20260729_203853_the_goal_you_specified_requires_a_project_to_execu.md @@ -0,0 +1,25 @@ +--- +type: "query" +date: "2026-07-29T20:38:53.488440+00:00" +question: "The goal you specified requires a project to execute but there is no POM in this directory (/workspace)." +contributor: "graphify" +outcome: "useful" +source_nodes: ["Workflow сборки и публикации Docker-образов", "Pipeline backend-образа", "Pipeline frontend-образа", "CI/CD Gitea Actions в инфраструктуре"] +--- + +# Q: The goal you specified requires a project to execute but there is no POM in this directory (/workspace). + +## Answer + +Expanded from original query via graph vocab: [actions, backend, build, docker, frontend, gitea, pipeline, test, workflow, workspace]. Корень сбоя: workflow передавал пути job-контейнера в bind mounts внешнего Docker daemon. Исправление: scripts/run-ci-container.sh создаёт временный контейнер, копирует исходники через Docker API, запускает команду и сохраняет её exit code; frontend дополнительно получает backend в /backend, а Trivy получает Docker config через DOCKER_CONFIG=/workspace без bind mount. Проверено полным Maven gate (274 теста) и frontend gate (63 теста). + +## Outcome + +- Signal: useful + +## Source Nodes + +- Workflow сборки и публикации Docker-образов +- Pipeline backend-образа +- Pipeline frontend-образа +- CI/CD Gitea Actions в инфраструктуре \ No newline at end of file diff --git a/scripts/check-artifact-pinning.sh b/scripts/check-artifact-pinning.sh index 83e024e..3ab9ca1 100755 --- a/scripts/check-artifact-pinning.sh +++ b/scripts/check-artifact-pinning.sh @@ -4,6 +4,8 @@ set -euo pipefail PROJECT_ROOT=${PROJECT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)} WORKFLOW="$PROJECT_ROOT/.gitea/workflows/docker-build.yaml" +CI_CONTAINER_SCRIPT="$PROJECT_ROOT/scripts/run-ci-container.sh" +CI_CONTAINER_TEST="$PROJECT_ROOT/scripts/test-run-ci-container.sh" failures=0 @@ -60,7 +62,7 @@ require_count() { local description=$3 local actual - actual=$(grep -Ec "$pattern" "$WORKFLOW" || true) + actual=$(grep -Ec -- "$pattern" "$WORKFLOW" || true) if (( actual < expected )); then fail "$description: ожидается не менее $expected, найдено $actual" fi @@ -68,6 +70,8 @@ require_count() { check_workflow_gates() { require_file "$WORKFLOW" || return + require_file "$CI_CONTAINER_SCRIPT" || return + require_file "$CI_CONTAINER_TEST" || return require_count '^[[:space:]]+sbom:[[:space:]]+true$' 2 \ "для обоих образов должна публиковаться SBOM attestation" @@ -81,6 +85,17 @@ check_workflow_gates() { "образ Maven для CI должен быть закреплён digest" require_count 'NODE_IMAGE: [^[:space:]]+@sha256:[0-9a-f]{64}$' 1 \ "образ Node.js для CI должен быть закреплён digest" + require_count 'bash scripts/run-ci-container\.sh' 4 \ + "тесты и Trivy должны передавать файлы через Docker API" + require_count 'bash scripts/test-run-ci-container\.sh' 1 \ + "workflow должен проверять helper запуска CI-контейнеров" + require_count '--env DOCKER_CONFIG=/workspace' 2 \ + "оба Trivy scan должны получать registry credentials через Docker API" + + if grep -Fq '${{ github.workspace }}' "$WORKFLOW" \ + || grep -Fq -- '-v "$HOME' "$WORKFLOW"; then + fail "нельзя использовать пути job-контейнера в bind mount внешнего Docker daemon" + fi if ! grep -Eq '^ needs: \[build-and-push-backend, build-and-push-frontend, security-scan\]$' "$WORKFLOW"; then fail "deploy обязан зависеть от успешного security-scan" diff --git a/scripts/run-ci-container.sh b/scripts/run-ci-container.sh new file mode 100755 index 0000000..7314f85 --- /dev/null +++ b/scripts/run-ci-container.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if (( $# < 4 )); then + printf 'Использование: %s <каталог-исходников> <образ> [--copy <каталог> <путь>] [--env <ИМЯ=значение>] -- <команда> [аргументы...]\n' \ + "${0##*/}" >&2 + exit 2 +fi + +SOURCE_DIR=$1 +IMAGE=$2 +shift 2 +EXTRA_SOURCE_DIRS=() +EXTRA_DESTINATIONS=() +CREATE_ARGS=() + +while (( $# > 0 )); do + case "$1" in + --copy) + if (( $# < 3 )); then + printf '[CI-КОНТЕЙНЕР] Ошибка: для --copy нужны каталог и путь в контейнере\n' >&2 + exit 2 + fi + EXTRA_SOURCE_DIRS+=("$2") + EXTRA_DESTINATIONS+=("$3") + shift 3 + ;; + --env) + if (( $# < 2 )) || [[ ! "$2" =~ ^[A-Za-z_][A-Za-z0-9_]*=.*$ ]]; then + printf '[CI-КОНТЕЙНЕР] Ошибка: для --env ожидается ИМЯ=значение\n' >&2 + exit 2 + fi + CREATE_ARGS+=(--env "$2") + shift 2 + ;; + --) + shift + break + ;; + *) + printf '[CI-КОНТЕЙНЕР] Ошибка: неизвестный аргумент до команды: %s\n' "$1" >&2 + exit 2 + ;; + esac +done + +if (( $# == 0 )); then + printf '[CI-КОНТЕЙНЕР] Ошибка: не указана команда контейнера\n' >&2 + exit 2 +fi + +if [[ ! -d "$SOURCE_DIR" ]]; then + printf '[CI-КОНТЕЙНЕР] Ошибка: каталог исходников не найден: %s\n' "$SOURCE_DIR" >&2 + exit 2 +fi +for index in "${!EXTRA_SOURCE_DIRS[@]}"; do + extra_source=${EXTRA_SOURCE_DIRS[$index]} + extra_destination=${EXTRA_DESTINATIONS[$index]} + if [[ ! -d "$extra_source" ]]; then + printf '[CI-КОНТЕЙНЕР] Ошибка: дополнительный каталог не найден: %s\n' \ + "$extra_source" >&2 + exit 2 + fi + if [[ "$extra_destination" != /* || "$extra_destination" == *..* ]]; then + printf '[CI-КОНТЕЙНЕР] Ошибка: путь назначения должен быть абсолютным без ..: %s\n' \ + "$extra_destination" >&2 + exit 2 + fi +done + +CONTAINER_ID= + +cleanup() { + if [[ -n "$CONTAINER_ID" ]]; then + docker rm --force "$CONTAINER_ID" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +CONTAINER_ID=$(docker create --workdir /workspace "${CREATE_ARGS[@]}" "$IMAGE" "$@") +docker cp "$SOURCE_DIR/." "$CONTAINER_ID:/workspace" +for index in "${!EXTRA_SOURCE_DIRS[@]}"; do + docker cp "${EXTRA_SOURCE_DIRS[$index]}/." \ + "$CONTAINER_ID:${EXTRA_DESTINATIONS[$index]}" +done +docker start --attach "$CONTAINER_ID" diff --git a/scripts/test-artifact-pinning.sh b/scripts/test-artifact-pinning.sh index 0200a88..90ba9d9 100755 --- a/scripts/test-artifact-pinning.sh +++ b/scripts/test-artifact-pinning.sh @@ -7,12 +7,20 @@ CHECK_SCRIPT="$ROOT_DIR/scripts/check-artifact-pinning.sh" FIXTURE_DIR=$(mktemp -d) trap 'rm -rf "$FIXTURE_DIR"' EXIT -mkdir -p "$FIXTURE_DIR/backend" "$FIXTURE_DIR/frontend" "$FIXTURE_DIR/.gitea/workflows" +mkdir -p \ + "$FIXTURE_DIR/backend" \ + "$FIXTURE_DIR/frontend" \ + "$FIXTURE_DIR/.gitea/workflows" \ + "$FIXTURE_DIR/scripts" cp "$ROOT_DIR/backend/Dockerfile" "$FIXTURE_DIR/backend/Dockerfile" cp "$ROOT_DIR/frontend/Dockerfile" "$FIXTURE_DIR/frontend/Dockerfile" cp "$ROOT_DIR/compose.yaml" "$FIXTURE_DIR/compose.yaml" cp "$ROOT_DIR/.gitea/workflows/docker-build.yaml" \ "$FIXTURE_DIR/.gitea/workflows/docker-build.yaml" +cp "$ROOT_DIR/scripts/run-ci-container.sh" \ + "$FIXTURE_DIR/scripts/run-ci-container.sh" +cp "$ROOT_DIR/scripts/test-run-ci-container.sh" \ + "$FIXTURE_DIR/scripts/test-run-ci-container.sh" PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null @@ -38,4 +46,13 @@ if PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null 2>&1; then exit 1 fi +cp "$ROOT_DIR/.gitea/workflows/docker-build.yaml" \ + "$FIXTURE_DIR/.gitea/workflows/docker-build.yaml" +sed -i '$a\# docker run --rm -v "${{ github.workspace }}/backend:/workspace"' \ + "$FIXTURE_DIR/.gitea/workflows/docker-build.yaml" +if PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null 2>&1; then + printf 'Ошибка теста: bind mount github.workspace не был отклонён\n' >&2 + exit 1 +fi + printf 'Проверка закрепления артефактов завершена успешно\n' diff --git a/scripts/test-run-ci-container.sh b/scripts/test-run-ci-container.sh new file mode 100755 index 0000000..b6c1826 --- /dev/null +++ b/scripts/test-run-ci-container.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +RUN_SCRIPT="$ROOT_DIR/scripts/run-ci-container.sh" +FIXTURE_DIR=$(mktemp -d) +CALL_LOG="$FIXTURE_DIR/docker-calls.log" +FAKE_BIN="$FIXTURE_DIR/bin" +SOURCE_DIR="$FIXTURE_DIR/source" +EXTRA_SOURCE_DIR="$FIXTURE_DIR/extra-source" +trap 'rm -rf "$FIXTURE_DIR"' EXIT + +mkdir -p "$FAKE_BIN" "$SOURCE_DIR" "$EXTRA_SOURCE_DIR" +printf 'fixture\n' > "$SOURCE_DIR/pom.xml" +printf 'extra fixture\n' > "$EXTRA_SOURCE_DIR/backend.java" + +cat > "$FAKE_BIN/docker" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >> "$DOCKER_CALL_LOG" +case "$1" in + create) + printf 'ci-container-id\n' + ;; + start) + exit "${DOCKER_START_EXIT_CODE:-0}" + ;; +esac +EOF +chmod +x "$FAKE_BIN/docker" + +DOCKER_CALL_LOG="$CALL_LOG" PATH="$FAKE_BIN:$PATH" \ + bash "$RUN_SCRIPT" "$SOURCE_DIR" 'registry.local/test@sha256:abc' \ + --copy "$EXTRA_SOURCE_DIR" /backend \ + --env DOCKER_CONFIG=/workspace \ + -- sh -c 'exit 0' + +grep --fixed-strings \ + 'create --workdir /workspace --env DOCKER_CONFIG=/workspace registry.local/test@sha256:abc sh -c exit 0' \ + "$CALL_LOG" >/dev/null +grep --fixed-strings \ + "cp $SOURCE_DIR/. ci-container-id:/workspace" \ + "$CALL_LOG" >/dev/null +grep --fixed-strings \ + "cp $EXTRA_SOURCE_DIR/. ci-container-id:/backend" \ + "$CALL_LOG" >/dev/null +grep --fixed-strings 'start --attach ci-container-id' "$CALL_LOG" >/dev/null +grep --fixed-strings 'rm --force ci-container-id' "$CALL_LOG" >/dev/null +if grep -Eq '(^|[[:space:]])(-v|--volume)([[:space:]]|=)' "$CALL_LOG"; then + printf 'Ошибка теста: helper использовал bind mount\n' >&2 + exit 1 +fi + +: > "$CALL_LOG" +set +e +DOCKER_CALL_LOG="$CALL_LOG" DOCKER_START_EXIT_CODE=17 PATH="$FAKE_BIN:$PATH" \ + bash "$RUN_SCRIPT" "$SOURCE_DIR" 'registry.local/test@sha256:abc' \ + -- sh -c 'exit 17' >/dev/null 2>&1 +status=$? +set -e + +if (( status != 17 )); then + printf 'Ошибка теста: код контейнера 17 превратился в %d\n' "$status" >&2 + exit 1 +fi +grep --fixed-strings 'rm --force ci-container-id' "$CALL_LOG" >/dev/null + +if PATH="$FAKE_BIN:$PATH" bash "$RUN_SCRIPT" "$FIXTURE_DIR/missing" \ + 'registry.local/test@sha256:abc' -- true >/dev/null 2>&1; then + printf 'Ошибка теста: отсутствующий каталог исходников не был отклонён\n' >&2 + exit 1 +fi + +printf 'Проверка запуска CI-контейнеров без bind mount завершена успешно\n'