diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..96e92a8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Tests (Python 3.11) + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: pyproject.toml + + - name: Upgrade pip + run: python -m pip install --upgrade pip + + - name: Install development dependencies + run: python -m pip install -e ".[dev]" + + - name: Run tests + run: python -m pytest -q diff --git a/README.md b/README.md index d3e2f65..0728856 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Statik kod analiz aracının ve bağımlılık tarayıcısının test edileceği - [Proje Kapsamı](docs/scope.md) - [Beş Haftalık Proje Planı](docs/project-plan.md) +- [GitHub Actions CI](docs/ci.md) ### Bileşen Dokümanları diff --git a/docs/README.md b/docs/README.md index cc832d9..34dd7bb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,7 @@ Bu klasörde projenin genel dokümanları ve üç ana bileşene ait dokümanlar - [Proje Kapsamı](scope.md) - [Beş Haftalık Proje Planı](project-plan.md) +- [GitHub Actions CI](ci.md) ## Bileşen Dokümanları diff --git a/docs/ci.md b/docs/ci.md new file mode 100644 index 0000000..773b52a --- /dev/null +++ b/docs/ci.md @@ -0,0 +1,143 @@ +# GitHub Actions CI + +Bu doküman Backlog 5.1 kapsamındaki temel test pipeline'ının gereksinimlerini, +güvenlik sınırlarını ve çalışma sırasını açıklar. + +## Mevcut Durum + +Temel GitHub Actions workflow'u tamamlanmıştır: + +```text +.github/workflows/ci.yml +``` + +Workflow Pull Request değişikliklerinde ve `main` branch push olaylarında +Python 3.11 test paketini çalıştırır. Sıradaki aşama static analyzer, +dependency scanner ve report artifact adımlarının ayrı CI job'ları olarak +eklenmesidir. + +## Trigger Sözleşmesi + +Pipeline aşağıdaki olaylarda çalışır: + +- Her `pull_request` olayı +- Yalnızca `main` branch'ine yapılan `push` olayları + +`pull_request_target` kullanılmaz. Böylece fork kaynaklı güvenilmeyen kod daha +yüksek yetkili base repository bağlamında çalıştırılmaz. + +## İzin ve Çalıştırma Sınırları + +Workflow-level `GITHUB_TOKEN` izni yalnızca şöyledir: + +```yaml +permissions: + contents: read +``` + +Checkout sonrasında credential persistence kapalıdır. Test job'u +`ubuntu-latest` runner üzerinde en fazla 10 dakika çalışır. Aynı workflow ve +Git ref için yeni bir run başladığında önceki run iptal edilir. + +Bu foundation job secret, write permission, deployment environment veya +external service credential kullanmaz. + +## Action Pinleri + +Supply-chain değişkenliğini azaltmak için action major tag'leri yerine release +commit SHA değerleri kullanılır: + +| Action | Release | Commit | +|---|---|---| +| `actions/checkout` | `v7.0.1` | `3d3c42e5aac5ba805825da76410c181273ba90b1` | +| `actions/setup-python` | `v7.0.0` | `5fda3b95a4ea91299a34e894583c3862153e4b97` | + +Yorumdaki release etiketi bakım sırasında SHA provenance değerinin kolayca +kontrol edilmesini sağlar. + +Resmî release kaynakları: + +- [actions/checkout v7.0.1](https://github.com/actions/checkout/releases/tag/v7.0.1) +- [actions/setup-python v7.0.0](https://github.com/actions/setup-python/releases/tag/v7.0.0) + +## Test Job Akışı + +Job sırası: + +1. Repository'yi immutable `actions/checkout` SHA değeriyle checkout et. +2. Python `3.11` ortamını immutable `actions/setup-python` SHA değeriyle kur. +3. `pyproject.toml` anahtarına göre pip download cache'ini geri yükle. +4. Pip'i güncelle. +5. Projeyi `.[dev]` extra bağımlılıklarıyla editable kur. +6. `python -m pytest -q` komutunu çalıştır. + +Install veya test komutlarından herhangi biri non-zero exit code üretirse job +ve workflow başarısız olur. + +## Yerel Eşdeğer + +CI test adımını yerelde doğrulamak için: + +```powershell +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +python -m pytest -q +``` + +Workflow sözleşmesi de normal test paketi içindedir: + +```powershell +python -m pytest tests/test_ci_workflow.py -q +``` + +Minimum Python syntax sözleşmesini doğrulamak için: + +```powershell +python -m pytest tests/test_python_compatibility.py -q +``` + +## Sözleşme Testleri + +`tests/test_ci_workflow.py` aşağıdaki değerleri korur: + +- Pull Request ve `main` push trigger'ları +- `pull_request_target` yasağı +- Salt-okunur repository permission +- Checkout credential persistence yasağı +- Concurrency cancellation +- İki action için tam 40 karakterli SHA ve release etiketi +- Python 3.11 ve pip cache yapılandırması +- Pip upgrade, dev install ve pytest komut sırası +- Ubuntu runner ve 10 dakikalık timeout + +`tests/test_python_compatibility.py`, `src`, `sample_app`, `tools` ve `tests` +altındaki bütün Python dosyalarını desteklenen en düşük sürüm olan Python +3.11 grameriyle ayrıştırır. Böylece daha yeni bir yorumlayıcıda yerel olarak +geçen Python 3.12+ söz dizimi değişiklikleri CI'a ulaşmadan tespit edilir. + +Doğrulanan mevcut sonuç: + +```text +CI workflow contract tests: 7 passed +Python 3.11 compatibility tests: 1 passed +Complete test suite: 985 passed +Workflow YAML parse check: passed +``` + +## Sonraki Güvenlik Job'ları + +Bu foundation PR yalnızca Backlog 5.1'i tamamlar. Sonraki aşamalar: + +1. `src` ve `tools` için sıfır-bulgu static self-analysis gate'i +2. `sample_app` için beklenen beş bulguyu drift testiyle doğrulama +3. Offline OSV fixture-backed dependency scan gate'i +4. Static ve dependency JSON raporlarını workflow artifact olarak yükleme + +Bu ayrım temel test workflow'unun ilk gerçek GitHub run sonucunu bağımsız +olarak incelemeyi sağlar. + +## Navigation + +- [Proje dokümantasyonuna dön](README.md) +- [Tüm bileşenlere dön](components/README.md) +- [Projenin ana sayfasına dön](../README.md) diff --git a/docs/components/sample-web-app/README.md b/docs/components/sample-web-app/README.md index 6785754..693e7ce 100644 --- a/docs/components/sample-web-app/README.md +++ b/docs/components/sample-web-app/README.md @@ -60,8 +60,9 @@ Mevcut özellikler: * Ayrı ve runtime dışı vulnerable dependency fixture'ı * Portable static-analysis ve dependency-scan JSON baseline'ları * Offline report generator ve drift check +* Pull Request ve `main` push için temel pytest CI workflow'u -Sıradaki aşama GitHub Actions CI/CD entegrasyonudur. +Sıradaki aşama analyzer, dependency scanner ve report artifact CI job'larıdır. ## Kurulum @@ -546,8 +547,10 @@ New update/delete test cases: 54 passed New frontend test cases: 18 passed New security demo test cases: 4 passed New integration report test cases: 6 passed +New CI workflow test cases: 7 passed +New Python 3.11 compatibility test cases: 1 passed Sample app targeted suite: 149 passed -Complete test suite: 977 passed +Complete test suite: 985 passed Compile check: passed Sample app analysis: 5 expected findings Analyzer source self-analysis: no findings diff --git a/docs/components/sample-web-app/analysis.md b/docs/components/sample-web-app/analysis.md index ade7707..813c4ca 100644 --- a/docs/components/sample-web-app/analysis.md +++ b/docs/components/sample-web-app/analysis.md @@ -108,7 +108,8 @@ Temel Flask uygulaması, Task modeli, in-memory store, JSON CRUD endpoint'leri, minimal Jinja/CSS frontend, kontrollü güvenlik fixture'ları ve uçtan uca entegrasyon raporları tamamlanmıştır. -Sıradaki geliştirme adımı analyzer araçlarının GitHub Actions pipeline'ına +Temel GitHub Actions pytest workflow'u da tamamlanmıştır. Sıradaki geliştirme +adımı analyzer, dependency scanner ve report artifact job'larının pipeline'a eklenmesidir. ## Flask Uygulama İskeleti Gereksinimleri diff --git a/docs/components/sample-web-app/technical-design.md b/docs/components/sample-web-app/technical-design.md index a59b9f1..7dabc24 100644 --- a/docs/components/sample-web-app/technical-design.md +++ b/docs/components/sample-web-app/technical-design.md @@ -201,12 +201,12 @@ Tamamlanan foundation çalışmaları: * Offline OSV dependency JSON baseline'ının eklenmesi * Tek komutlu report generator ve drift check'in eklenmesi * Uçtan uca demo sırasının belgelenmesi +* GitHub Actions pytest foundation workflow'unun eklenmesi Sıradaki geliştirmeler: -* GitHub Actions workflow'unun eklenmesi -* CI Python ortamı ve test job'larının yapılandırılması * Analyzer ve dependency scanner CI adımlarının eklenmesi +* JSON security report artifact upload adımının eklenmesi ## Flask Foundation Teknik Tasarımı diff --git a/src/static_analyzer/analysis_engine.py b/src/static_analyzer/analysis_engine.py index 263fc00..0587cca 100644 --- a/src/static_analyzer/analysis_engine.py +++ b/src/static_analyzer/analysis_engine.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Iterable +from typing import TypeAlias from static_analyzer.models import Finding from static_analyzer.rules.base import BaseRule @@ -10,7 +11,7 @@ from static_analyzer.source_reader import SourceFile -type AnalysisRule = BaseRule | BaseTextRule +AnalysisRule: TypeAlias = BaseRule | BaseTextRule class AnalysisEngine: @@ -47,4 +48,4 @@ def analyze( findings.extend(rule_findings) - return findings \ No newline at end of file + return findings diff --git a/tests/test_ci_workflow.py b/tests/test_ci_workflow.py new file mode 100644 index 0000000..622c715 --- /dev/null +++ b/tests/test_ci_workflow.py @@ -0,0 +1,125 @@ +"""Contract tests for the GitHub Actions CI foundation.""" + +from __future__ import annotations + +import re +from pathlib import Path + + +_REPOSITORY_ROOT = Path(__file__).parents[1] +_WORKFLOW_PATH = ( + _REPOSITORY_ROOT / ".github" / "workflows" / "ci.yml" +) +_ACTION_PATTERN = re.compile( + r"uses: " + r"(?Pactions/(?:checkout|setup-python))" + r"@(?P[0-9a-f]{40})" + r" # (?Pv[0-9]+\.[0-9]+\.[0-9]+)" +) + + +def _workflow_text() -> str: + """Return the checked-in workflow document.""" + + return _WORKFLOW_PATH.read_text(encoding="utf-8") + + +def test_workflow_runs_for_pull_requests_and_main_pushes() -> None: + """CI should cover review changes and merged main commits.""" + + workflow = _workflow_text() + + assert ( + "on:\n" + " pull_request:\n" + " push:\n" + " branches:\n" + " - main\n" + ) in workflow + assert "pull_request_target" not in workflow + + +def test_workflow_uses_read_only_repository_permissions() -> None: + """The default token should not receive write permissions.""" + + workflow = _workflow_text() + + assert "permissions:\n contents: read\n" in workflow + assert not re.search( + r"(?m)^\s+[a-z-]+: write\s*$", + workflow, + ) + assert "persist-credentials: false" in workflow + + +def test_workflow_cancels_superseded_branch_runs() -> None: + """New commits should replace obsolete work on the same ref.""" + + workflow = _workflow_text() + + assert ( + "group: ci-${{ github.workflow }}-${{ github.ref }}" + in workflow + ) + assert "cancel-in-progress: true" in workflow + + +def test_official_actions_are_pinned_to_expected_commits() -> None: + """Third-party execution should use reviewed immutable SHAs.""" + + matches = { + match.group("action"): ( + match.group("sha"), + match.group("version"), + ) + for match in _ACTION_PATTERN.finditer( + _workflow_text() + ) + } + + assert matches == { + "actions/checkout": ( + "3d3c42e5aac5ba805825da76410c181273ba90b1", + "v7.0.1", + ), + "actions/setup-python": ( + "5fda3b95a4ea91299a34e894583c3862153e4b97", + "v7.0.0", + ), + } + + +def test_python_environment_is_explicit_and_cached() -> None: + """CI should reproduce the supported Python setup efficiently.""" + + workflow = _workflow_text() + + assert 'python-version: "3.11"' in workflow + assert "cache: pip" in workflow + assert "cache-dependency-path: pyproject.toml" in workflow + + +def test_dependency_install_and_test_commands_are_ordered() -> None: + """The job should prepare pip, install dev extras, then test.""" + + workflow = _workflow_text() + commands = ( + "python -m pip install --upgrade pip", + 'python -m pip install -e ".[dev]"', + "python -m pytest -q", + ) + positions = tuple( + workflow.index(command) + for command in commands + ) + + assert positions == tuple(sorted(positions)) + + +def test_test_job_has_bounded_ubuntu_execution() -> None: + """A stuck test run should not consume an unbounded runner.""" + + workflow = _workflow_text() + + assert "runs-on: ubuntu-latest" in workflow + assert "timeout-minutes: 10" in workflow diff --git a/tests/test_python_compatibility.py b/tests/test_python_compatibility.py new file mode 100644 index 0000000..d3b52d1 --- /dev/null +++ b/tests/test_python_compatibility.py @@ -0,0 +1,42 @@ +"""Tests for the declared minimum Python syntax compatibility.""" + +from __future__ import annotations + +import ast +from pathlib import Path + +import pytest + + +_REPOSITORY_ROOT = Path(__file__).parents[1] +_PYTHON_SOURCE_ROOTS = ( + _REPOSITORY_ROOT / "src", + _REPOSITORY_ROOT / "sample_app", + _REPOSITORY_ROOT / "tools", + _REPOSITORY_ROOT / "tests", +) +_MINIMUM_PYTHON_VERSION = (3, 11) + + +def test_all_python_sources_use_python_3_11_syntax() -> None: + """Every checked-in Python file should parse with Python 3.11.""" + + for source_root in _PYTHON_SOURCE_ROOTS: + for file_path in sorted(source_root.rglob("*.py")): + source = file_path.read_text(encoding="utf-8") + + try: + ast.parse( + source, + filename=str(file_path), + feature_version=_MINIMUM_PYTHON_VERSION, + ) + except SyntaxError as error: + relative_path = file_path.relative_to( + _REPOSITORY_ROOT + ) + pytest.fail( + f"{relative_path} is not compatible with " + "Python 3.11 syntax: " + f"{error.msg} at line {error.lineno}." + )