Skip to content

Commit 89661f8

Browse files
authored
feat(ci): set up GitHub Actions CI/CD with pytest matrix, lint, security scan (#49)
Establishes a production-grade CI/CD pipeline so every PR enforces the same quality gates: cross-platform test execution, static analysis, and security scanning. Publishing to PyPI is now a declarative, artifact-driven release workflow triggered by GitHub Releases. Workflows: - test.yml: pytest matrix on ubuntu-latest and windows-latest across Python 3.11, 3.12, 3.13. Installs the package with the new `dev` extras (pip install -e ".[dev,blockchain]"), runs pytest with branch coverage across services, tools, auth, blockchain, and api, uploads coverage XML to Codecov from a single ubuntu/3.12 job to avoid duplicate uploads, and keeps the existing crypto / merkle / SSRF smoke tests. - lint.yml: ruff check (blocking) plus mypy (advisory) so style issues fail fast while gradual type adoption does not block merges. Pinned to ruff >= 0.6.0 and the shared pyproject.toml ruff config. - security.yml: pip-audit for dependency CVEs (advisory on PR, strict on main/schedule), bandit medium+ severity/confidence SAST, and safety as an advisory CVE scanner. Also runs weekly via cron (Monday 06:00 UTC). - publish.yml: build -> twine check -> upload-artifact pipeline, then a separate publish-pypi job using the PYPI_API_TOKEN secret with skip-existing, and an attach-release-assets job that attaches the built sdist and wheel to the GitHub Release. Project config: - pyproject.toml: add `dev`, `lint`, and `security` optional-dependency groups so contributors can `pip install -e ".[dev]"` locally and match CI exactly. Add pytest-cov to the `test` extra. Add tool sections for ruff (conservative initial rule set so CI passes without a repo-wide style rewrite), mypy (permissive baseline), bandit (excludes tests, examples, demos), and coverage (branch coverage, source list). - README.md: replace the hard-coded version badge with live CI, lint, security, and Codecov badges. Add a PyPI download count badge. Tooling notes: - actions/checkout@v4, actions/setup-python@v5, codecov/codecov-action@v5, actions/upload-artifact@v4 and download-artifact@v4 used throughout. - No `os.chmod` / POSIX-specific tests are present in the suite today, so no platform skips are required for the Windows matrix leg. - pytest-asyncio configured via asyncio_mode = "auto" already, so the Windows runners inherit the same event-loop policy as Linux.
1 parent 88ce5fa commit 89661f8

6 files changed

Lines changed: 394 additions & 41 deletions

File tree

.github/workflows/lint.yml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Lint
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: lint-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
ruff:
19+
name: ruff check
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Checkout repository
23+
uses: actions/checkout@v4
24+
25+
- name: Set up Python
26+
uses: actions/setup-python@v5
27+
with:
28+
python-version: '3.12'
29+
cache: 'pip'
30+
cache-dependency-path: pyproject.toml
31+
32+
- name: Install lint dependencies
33+
run: |
34+
python -m pip install --upgrade pip
35+
pip install "ruff>=0.6.0"
36+
37+
- name: Run ruff check
38+
run: ruff check . --output-format=github
39+
40+
- name: Run ruff format (check only, advisory)
41+
run: ruff format --check . || echo "Formatter differences present (non-blocking)."
42+
continue-on-error: true
43+
44+
mypy:
45+
name: mypy (advisory)
46+
runs-on: ubuntu-latest
47+
steps:
48+
- name: Checkout repository
49+
uses: actions/checkout@v4
50+
51+
- name: Set up Python
52+
uses: actions/setup-python@v5
53+
with:
54+
python-version: '3.12'
55+
cache: 'pip'
56+
cache-dependency-path: pyproject.toml
57+
58+
- name: Install dependencies
59+
run: |
60+
python -m pip install --upgrade pip
61+
pip install -e ".[dev,blockchain]"
62+
63+
- name: Run mypy
64+
# Advisory only: do not block merges while type coverage is being added.
65+
run: mypy services tools auth blockchain api --no-error-summary || true

.github/workflows/publish.yml

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,90 @@ name: Publish to PyPI
33
on:
44
release:
55
types: [published]
6+
workflow_dispatch:
67

78
permissions:
9+
contents: write
810
id-token: write
9-
contents: read
1011

1112
jobs:
12-
publish:
13+
build:
14+
name: Build sdist and wheel
1315
runs-on: ubuntu-latest
14-
environment:
15-
name: pypi
16-
url: https://pypi.org/project/attestix/
16+
outputs:
17+
version: ${{ steps.version.outputs.version }}
1718
steps:
18-
- uses: actions/checkout@v4
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
1921

20-
- uses: actions/setup-python@v5
22+
- name: Set up Python
23+
uses: actions/setup-python@v5
2124
with:
2225
python-version: '3.12'
26+
cache: 'pip'
27+
cache-dependency-path: pyproject.toml
2328

24-
- name: Install build tools
25-
run: pip install build
29+
- name: Install build tooling
30+
run: |
31+
python -m pip install --upgrade pip
32+
pip install build twine
2633
27-
- name: Build package
34+
- name: Build distributions
2835
run: python -m build
2936

30-
- name: Publish to PyPI
37+
- name: Verify distributions with twine
38+
run: twine check dist/*
39+
40+
- name: Extract package version
41+
id: version
42+
run: |
43+
VERSION=$(python -c "import tomllib, pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
44+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
45+
46+
- name: Upload build artifacts
47+
uses: actions/upload-artifact@v4
48+
with:
49+
name: python-package-distributions
50+
path: dist/
51+
if-no-files-found: error
52+
retention-days: 14
53+
54+
publish-pypi:
55+
name: Publish to PyPI
56+
needs: build
57+
runs-on: ubuntu-latest
58+
environment:
59+
name: pypi
60+
url: https://pypi.org/project/attestix/
61+
steps:
62+
- name: Download build artifacts
63+
uses: actions/download-artifact@v4
64+
with:
65+
name: python-package-distributions
66+
path: dist/
67+
68+
- name: Publish via API token
69+
# Uses PYPI_API_TOKEN secret. Trusted Publishing (id-token) can be
70+
# enabled later by dropping the password field.
3171
uses: pypa/gh-action-pypi-publish@release/v1
72+
with:
73+
password: ${{ secrets.PYPI_API_TOKEN }}
74+
skip-existing: true
75+
verbose: true
76+
77+
attach-release-assets:
78+
name: Attach dist to GitHub Release
79+
needs: build
80+
if: github.event_name == 'release'
81+
runs-on: ubuntu-latest
82+
steps:
83+
- name: Download build artifacts
84+
uses: actions/download-artifact@v4
85+
with:
86+
name: python-package-distributions
87+
path: dist/
88+
89+
- name: Upload dist/* to release
90+
uses: softprops/action-gh-release@v2
91+
with:
92+
files: dist/*

.github/workflows/security.yml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
name: Security
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
schedule:
9+
# Weekly: every Monday at 06:00 UTC.
10+
- cron: '0 6 * * 1'
11+
workflow_dispatch:
12+
13+
permissions:
14+
contents: read
15+
16+
concurrency:
17+
group: security-${{ github.ref }}
18+
cancel-in-progress: true
19+
20+
jobs:
21+
pip-audit:
22+
name: pip-audit (dependencies)
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout repository
26+
uses: actions/checkout@v4
27+
28+
- name: Set up Python
29+
uses: actions/setup-python@v5
30+
with:
31+
python-version: '3.12'
32+
cache: 'pip'
33+
cache-dependency-path: pyproject.toml
34+
35+
- name: Install package and pip-audit
36+
run: |
37+
python -m pip install --upgrade pip
38+
pip install -e ".[blockchain]"
39+
pip install "pip-audit>=2.7"
40+
41+
- name: Run pip-audit
42+
# Advisory on PR, enforced on schedule + push to main.
43+
run: |
44+
if [ "${{ github.event_name }}" = "pull_request" ]; then
45+
pip-audit --progress-spinner off || echo "pip-audit findings present (advisory on PR)."
46+
else
47+
pip-audit --progress-spinner off --strict
48+
fi
49+
50+
bandit:
51+
name: bandit (source code SAST)
52+
runs-on: ubuntu-latest
53+
steps:
54+
- name: Checkout repository
55+
uses: actions/checkout@v4
56+
57+
- name: Set up Python
58+
uses: actions/setup-python@v5
59+
with:
60+
python-version: '3.12'
61+
cache: 'pip'
62+
cache-dependency-path: pyproject.toml
63+
64+
- name: Install bandit
65+
run: |
66+
python -m pip install --upgrade pip
67+
pip install "bandit[toml]>=1.7"
68+
69+
- name: Run bandit (medium+ severity, medium+ confidence)
70+
run: bandit -r services tools auth blockchain api -c pyproject.toml -ll -ii
71+
72+
safety:
73+
name: safety (CVE scan, advisory)
74+
runs-on: ubuntu-latest
75+
continue-on-error: true
76+
steps:
77+
- name: Checkout repository
78+
uses: actions/checkout@v4
79+
80+
- name: Set up Python
81+
uses: actions/setup-python@v5
82+
with:
83+
python-version: '3.12'
84+
cache: 'pip'
85+
cache-dependency-path: pyproject.toml
86+
87+
- name: Install safety
88+
run: |
89+
python -m pip install --upgrade pip
90+
pip install "safety>=3.2"
91+
92+
- name: Run safety check
93+
run: safety check --file=requirements.txt --full-report || true

.github/workflows/test.yml

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Test Suite
1+
name: Tests
22

33
on:
44
push:
@@ -16,44 +16,64 @@ concurrency:
1616

1717
jobs:
1818
test:
19-
runs-on: ubuntu-latest
19+
name: pytest (${{ matrix.os }}, py${{ matrix.python-version }})
20+
runs-on: ${{ matrix.os }}
2021
strategy:
2122
fail-fast: false
2223
matrix:
23-
python-version: ['3.10', '3.11', '3.12', '3.13']
24+
os: [ubuntu-latest, windows-latest]
25+
python-version: ['3.11', '3.12', '3.13']
2426

2527
steps:
26-
- uses: actions/checkout@v4
28+
- name: Checkout repository
29+
uses: actions/checkout@v4
2730

28-
- uses: actions/setup-python@v5
31+
- name: Set up Python ${{ matrix.python-version }}
32+
uses: actions/setup-python@v5
2933
with:
3034
python-version: ${{ matrix.python-version }}
3135
cache: 'pip'
36+
cache-dependency-path: |
37+
pyproject.toml
38+
requirements.txt
3239
33-
- name: Install dependencies
34-
run: pip install -e ".[test,blockchain]"
40+
- name: Upgrade pip
41+
run: python -m pip install --upgrade pip
3542

36-
- name: Run test suite
37-
run: python -m pytest tests/ -v --tb=short -m "not live_blockchain" --color=yes
43+
- name: Install package with dev extras
44+
run: pip install -e ".[dev,blockchain]"
45+
46+
- name: Run pytest with coverage
47+
env:
48+
PYTHONIOENCODING: utf-8
49+
run: >-
50+
python -m pytest tests/
51+
-v --tb=short --color=yes
52+
-m "not live_blockchain"
53+
--cov=services --cov=tools --cov=auth --cov=blockchain --cov=api
54+
--cov-report=xml --cov-report=term-missing
3855
3956
- name: Smoke tests
57+
shell: bash
4058
run: |
41-
python -c "
42-
from auth.crypto import generate_ed25519_keypair, sign_message, verify_signature, public_key_to_did_key
43-
k, p = generate_ed25519_keypair()
44-
s = sign_message(k, b'test')
45-
assert verify_signature(p, s, b'test')
46-
d = public_key_to_did_key(p)
47-
print(f'Crypto OK - DID {d[:30]}...')
48-
"
49-
python -c "
50-
from blockchain.merkle import build_merkle_tree
51-
root, levels = build_merkle_tree([b'a', b'b', b'c'])
52-
print(f'Merkle OK - root {root[:16]}...')
53-
"
54-
python -c "
55-
from auth.ssrf import validate_url_host
56-
assert validate_url_host('127.0.0.1') is not None
57-
assert validate_url_host('localhost') is not None
58-
print('SSRF OK')
59-
"
59+
python -c "from auth.crypto import generate_ed25519_keypair, sign_message, verify_signature, public_key_to_did_key; k, p = generate_ed25519_keypair(); s = sign_message(k, b'test'); assert verify_signature(p, s, b'test'); d = public_key_to_did_key(p); print('Crypto OK', d[:30])"
60+
python -c "from blockchain.merkle import build_merkle_tree; root, levels = build_merkle_tree([b'a', b'b', b'c']); print('Merkle OK', root[:16])"
61+
python -c "from auth.ssrf import validate_url_host; assert validate_url_host('127.0.0.1') is not None; assert validate_url_host('localhost') is not None; print('SSRF OK')"
62+
63+
- name: Upload coverage to Codecov
64+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
65+
uses: codecov/codecov-action@v5
66+
with:
67+
files: ./coverage.xml
68+
flags: unittests
69+
name: attestix-coverage
70+
fail_ci_if_error: false
71+
token: ${{ secrets.CODECOV_TOKEN }}
72+
73+
- name: Upload coverage artifact
74+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
75+
uses: actions/upload-artifact@v4
76+
with:
77+
name: coverage-xml
78+
path: coverage.xml
79+
if-no-files-found: ignore

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
</p>
1212

1313
<p align="center">
14-
<a href="https://github.com/VibeTensor/attestix/releases"><img src="https://img.shields.io/badge/version-0.2.5-4f46e5?style=flat-square" alt="v0.2.4"></a>
14+
<a href="https://github.com/VibeTensor/attestix/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/VibeTensor/attestix/test.yml?branch=main&label=tests&style=flat-square" alt="CI"></a>
15+
<a href="https://github.com/VibeTensor/attestix/actions/workflows/lint.yml"><img src="https://img.shields.io/github/actions/workflow/status/VibeTensor/attestix/lint.yml?branch=main&label=lint&style=flat-square" alt="Lint"></a>
16+
<a href="https://github.com/VibeTensor/attestix/actions/workflows/security.yml"><img src="https://img.shields.io/github/actions/workflow/status/VibeTensor/attestix/security.yml?branch=main&label=security&style=flat-square" alt="Security"></a>
17+
<a href="https://codecov.io/gh/VibeTensor/attestix"><img src="https://img.shields.io/codecov/c/github/VibeTensor/attestix?style=flat-square" alt="Coverage"></a>
1518
<a href="https://pypi.org/project/attestix/"><img src="https://img.shields.io/pypi/v/attestix?color=4f46e5&style=flat-square" alt="PyPI"></a>
1619
<a href="https://pypi.org/project/attestix/"><img src="https://img.shields.io/pypi/pyversions/attestix?color=4f46e5&style=flat-square" alt="Python"></a>
20+
<a href="https://pypi.org/project/attestix/"><img src="https://img.shields.io/pypi/dm/attestix?color=4f46e5&style=flat-square" alt="Downloads"></a>
1721
<a href="https://github.com/VibeTensor/attestix/blob/main/LICENSE"><img src="https://img.shields.io/github/license/VibeTensor/attestix?color=4f46e5&style=flat-square" alt="License"></a>
1822
<a href="https://attestix.io/docs"><img src="https://img.shields.io/badge/docs-attestix.io%2Fdocs-4f46e5?style=flat-square" alt="Docs"></a>
1923
<a href="https://attestix.io"><img src="https://img.shields.io/badge/website-attestix.io-4f46e5?style=flat-square" alt="Website"></a>

0 commit comments

Comments
 (0)