ci: пайплайн с гейтами (lint, unit+coverage>=70%, security, build, image-scan, push staging-<sha>)
contract-openapi / openapi-contract (push) Failing after 0s
contract-openapi / ts-client-is-current (push) Failing after 1s
ci / lint (push) Successful in 7m38s
ci / unit (push) Failing after 1m3s
ci / security (push) Successful in 2m19s
ci / build (push) Has been skipped
ci / image-scan (push) Has been skipped
ci / push (push) Has been skipped
contract-openapi / openapi-contract (push) Failing after 0s
contract-openapi / ts-client-is-current (push) Failing after 1s
ci / lint (push) Successful in 7m38s
ci / unit (push) Failing after 1m3s
ci / security (push) Successful in 2m19s
ci / build (push) Has been skipped
ci / image-scan (push) Has been skipped
ci / push (push) Has been skipped
Refs #17, Refs #15 - lint: ruff check/format + mypy strict - unit: pytest с --cov-fail-under=70, артефакты coverage.xml + htmlcov - security: bandit -lll (HIGH) + pip-audit, артефакты bandit/pip-audit - build: docker build deploy/Dockerfile -> staging-<sha> (активируется после merge #18) - image-scan: trivy HIGH/CRITICAL --exit-code 1 - push: docker login + push в registry 192.168.1.111:3000 Обход дефекта Gitea git-http (upload-pack отдаёт пустой pack): fallback на archive/<sha>.tar.gz, actions/checkout остаётся основным путём.
This commit is contained in:
@@ -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-<sha>
|
||||||
|
# Гейты (роняют 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/<sha>.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-<sha> в 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-<sha>"
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user