name: ci # CI-пайплайн с гейтами (issue #17, Refs #15) # lint -> unit (coverage >= 70%) -> security (bandit/pip-audit) -> build -> image-scan (trivy) -> push staging- # Гейты (роняют pipeline): # * unit — покрытие < 70 % (--cov-fail-under=70) # * security — bandit HIGH, pip-audit: любая известная уязвимость # * image-scan — trivy: HIGH/CRITICAL в образе # * build стартует только после зелёных lint+unit+security; push — только после зелёного image-scan. on: push: branches: ["feat/**", "fix/**", "chore/**", "ci/**", "main"] pull_request: branches: [main] workflow_dispatch: env: PYTHON_VERSION: "3.12" REGISTRY: 192.168.1.111:3000 IMAGE_NAME: ${{ github.repository }} jobs: # ─────────────────────────────── 1. LINT ──────────────────────────────── lint: name: lint runs-on: ubuntu-latest outputs: has_dockerfile: ${{ steps.dockerfile.outputs.has_dockerfile }} steps: - name: checkout id: checkout uses: actions/checkout@v4 continue-on-error: true # Дефект Gitea git-http: upload-pack отдаёт пустой pack (Content-Length: 90), # git clone/fetch падает "early EOF / invalid index-pack output". # Обход: эндпоинт archive/.tar.gz работает — забираем исходники им. # Когда git-http починят, шаг просто перестанет срабатывать (outcome != failure). - name: "checkout fallback (archive tarball)" if: steps.checkout.outcome == 'failure' run: | set -euo pipefail curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz" tar -xzf /tmp/src.tar.gz --strip-components=1 -C . echo "sources: archive fallback, $(ls -1 | wc -l) entries" - name: "python 3.12" uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-dev.txt -r requirements-contract.txt - name: ruff check run: ruff check backend - name: ruff format --check run: ruff format --check backend - name: "mypy (strict)" run: mypy backend - name: detect deploy/Dockerfile id: dockerfile run: | if [ -f deploy/Dockerfile ]; then echo "has_dockerfile=true" >> "$GITHUB_OUTPUT" echo "deploy/Dockerfile найден — jobs build/image-scan/push активны" else echo "has_dockerfile=false" >> "$GITHUB_OUTPUT" echo "::warning::deploy/Dockerfile отсутствует — build/image-scan/push пропущены (ждут merge #18 / PR #49)" fi # ─────────────────── 2. UNIT + ГЕЙТ ПОКРЫТИЯ >= 70 % ─────────────────── unit: name: unit needs: lint runs-on: ubuntu-latest steps: - name: checkout id: checkout uses: actions/checkout@v4 continue-on-error: true - name: "checkout fallback (archive tarball)" if: steps.checkout.outcome == 'failure' run: | set -euo pipefail curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz" tar -xzf /tmp/src.tar.gz --strip-components=1 -C . - name: "python 3.12" uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-dev.txt -r requirements-contract.txt - name: "pytest + coverage gate (fail below 70%)" run: | python -m pytest \ --cov=awg_agent --cov=awg_api \ --cov-report=term-missing \ --cov-report=xml:coverage.xml \ --cov-report=html:htmlcov \ --cov-fail-under=70 - name: coverage summary if: always() run: python -m coverage report --sort=cover - name: upload coverage artifacts if: always() uses: actions/upload-artifact@v3 with: name: coverage path: | coverage.xml htmlcov/ retention-days: 14 # ───────────── 3. SECURITY: bandit (HIGH) + pip-audit ────────────────── security: name: security needs: lint runs-on: ubuntu-latest steps: - name: checkout id: checkout uses: actions/checkout@v4 continue-on-error: true - name: "checkout fallback (archive tarball)" if: steps.checkout.outcome == 'failure' run: | set -euo pipefail curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz" tar -xzf /tmp/src.tar.gz --strip-components=1 -C . - name: "python 3.12" uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: install scanners run: | python -m pip install --upgrade pip pip install "bandit[toml]>=1.7" "pip-audit>=2.7" pip install -r requirements.txt -r requirements-contract.txt - name: "bandit -lll (HIGH severity gate)" run: | set -o pipefail bandit -lll -r backend 2>&1 | tee bandit-report.txt bandit -lll -f json -o bandit-report.json -r backend || true echo "bandit: HIGH/CRITICAL не найдены" - name: "pip-audit (known vulnerabilities gate)" run: | pip-audit -r requirements.txt -r requirements-contract.txt --progress-spinner off - name: pip-audit json report if: always() run: pip-audit -r requirements.txt -r requirements-contract.txt --progress-spinner off --format json -o pip-audit-report.json || true - name: upload security artifacts if: always() uses: actions/upload-artifact@v3 with: name: security-reports path: | bandit-report.txt bandit-report.json pip-audit-report.json retention-days: 14 # ───────────────── 4. BUILD: docker build (нужен #18) ────────────────── build: name: build needs: [lint, unit, security] if: needs.lint.outputs.has_dockerfile == 'true' runs-on: ubuntu-latest steps: - name: checkout id: checkout uses: actions/checkout@v4 continue-on-error: true - name: "checkout fallback (archive tarball)" if: steps.checkout.outcome == 'failure' run: | set -euo pipefail curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz" tar -xzf /tmp/src.tar.gz --strip-components=1 -C . - name: "guard: deploy/Dockerfile" run: | if [ ! -f deploy/Dockerfile ]; then echo "::error::deploy/Dockerfile отсутствует: сборка образа ждёт merge #18 (PR #49)." echo "Пайплайн целиком проверен на ветке feat/17-verify (она же + deploy/Dockerfile)." exit 1 fi - name: image meta id: meta run: | SHA_SHORT="${GITHUB_SHA::8}" IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${SHA_SHORT}" echo "sha_short=${SHA_SHORT}" >> "$GITHUB_OUTPUT" echo "image=${IMAGE}" >> "$GITHUB_OUTPUT" echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV" echo "image: ${IMAGE}" - name: docker build run: | set -o pipefail docker build --pull -f deploy/Dockerfile -t "$IMAGE" . 2>&1 | tee build.log - name: image digest run: | docker image inspect "$IMAGE" --format 'id={{.Id}} size={{.Size}}' docker image inspect "$IMAGE" --format '{{.Id}}' > image-id.txt - name: upload build artifacts if: always() uses: actions/upload-artifact@v3 with: name: "build-${{ github.sha }}" path: | build.log image-id.txt retention-days: 14 # ──────── 5. IMAGE-SCAN: trivy, гейт HIGH/CRITICAL ──────────────────── image-scan: name: image-scan needs: [lint, build] if: needs.lint.outputs.has_dockerfile == 'true' runs-on: ubuntu-latest env: TRIVY_VERSION: "0.76.1" TRIVY_SEVERITY: "HIGH,CRITICAL" steps: # trivy запускается официальным образом через docker-сокет: не требует # скачивания бинаря (github releases отдаёт медленно) + БД кэшируется в # /opt/trivy-cache на build-хосте. - name: "trivy image (HIGH/CRITICAL -> fail)" run: | set -o pipefail IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}" echo "scan: ${IMAGE}" docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /opt/trivy-cache:/root/.cache/trivy \ "aquasec/trivy:${TRIVY_VERSION}" image \ --image-src docker --scanners vuln \ --severity "$TRIVY_SEVERITY" --ignore-unfixed \ --exit-code 1 --format table "$IMAGE" 2>&1 | tee trivy-report.txt - name: trivy json report if: always() run: | set -o pipefail IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}" mkdir -p reports docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /opt/trivy-cache:/root/.cache/trivy \ -v "$PWD/reports:/reports" \ "aquasec/trivy:${TRIVY_VERSION}" image \ --image-src docker --scanners vuln \ --severity "$TRIVY_SEVERITY" --ignore-unfixed \ --format json -o /reports/trivy-report.json "$IMAGE" || true cp reports/trivy-report.json ./trivy-report.json 2>/dev/null || true - name: upload trivy report if: always() uses: actions/upload-artifact@v3 with: name: trivy-report path: | trivy-report.txt trivy-report.json retention-days: 14 # ─────── 6. PUSH: staging- в registry Gitea ────────────────────── push: name: push needs: [lint, build, image-scan] if: needs.lint.outputs.has_dockerfile == 'true' && github.event_name == 'push' runs-on: ubuntu-latest steps: - name: docker login run: | set -o pipefail # Приоритет — секрет с правом write:package (REGISTRY_TOKEN), иначе встроенный токен Actions. TOKEN="${{ secrets.REGISTRY_TOKEN }}" if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.GITHUB_TOKEN }}" fi echo "$TOKEN" | docker login "$REGISTRY" -u "${{ github.actor }}" --password-stdin - name: "push staging-" run: | set -o pipefail IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}" docker push "$IMAGE" 2>&1 | tee push.log echo "${IMAGE}" | tee pushed-image.txt - name: upload push record if: always() uses: actions/upload-artifact@v3 with: name: pushed-image path: | push.log pushed-image.txt retention-days: 14