diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..7616b46 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,308 @@ +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 + + - 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 + + - 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 + + - 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 --progress-spinner off + + - name: pip-audit json report + if: always() + run: pip-audit -r requirements.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: 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.57.1" + TRIVY_SEVERITY: "HIGH,CRITICAL" + steps: + - name: install trivy + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \ + | sh -s -- -b /usr/local/bin "v${TRIVY_VERSION}" + trivy --version + + - name: "trivy image (HIGH/CRITICAL -> fail)" + run: | + set -o pipefail + IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}" + echo "scan: ${IMAGE}" + trivy 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: | + IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}" + trivy image --image-src docker --scanners vuln --severity "$TRIVY_SEVERITY" \ + --ignore-unfixed --format json -o trivy-report.json "$IMAGE" || 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 + echo "${{ secrets.GITHUB_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