Skip to content

Commit a164e40

Browse files
authored
Merge pull request #234 from dgenio/claude/issue-triage-grouping-uz3fij
ci: CI & supply-chain hardening (#141, #195, #202, #205, #208, #209, #210, #212, #225, #232)
2 parents 03fa0e2 + dd46e5a commit a164e40

21 files changed

Lines changed: 1022 additions & 52 deletions

.github/dependabot.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
version: 2
2+
updates:
3+
# Python dependencies. The library declares *ranged* requirements
4+
# (httpx>=0.27, pydantic>=2) on purpose; Dependabot only proposes a change
5+
# when a dependency drifts out of the declared range, so it never forces a
6+
# pin on the library itself. Minor/patch bumps are grouped to limit noise.
7+
- package-ecosystem: "pip"
8+
directory: "/"
9+
schedule:
10+
interval: "weekly"
11+
open-pull-requests-limit: 5
12+
groups:
13+
python-minor-and-patch:
14+
update-types:
15+
- "minor"
16+
- "patch"
17+
18+
# GitHub Actions. Keeps the SHA-pinned actions in ci.yml / publish.yml /
19+
# codeql.yml fresh (it updates the pin and the version comment together).
20+
- package-ecosystem: "github-actions"
21+
directory: "/"
22+
schedule:
23+
interval: "weekly"
24+
open-pull-requests-limit: 5
25+
groups:
26+
github-actions:
27+
patterns:
28+
- "*"

.github/workflows/ci.yml

Lines changed: 150 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
name: CI
22

3+
# Contract: this workflow runs the same gate as `make ci`
4+
# (fmt-check -> lint -> type -> test -> example). Each step below invokes a
5+
# Makefile target so the local gate and CI cannot drift (see Makefile and
6+
# docs/agent-context/workflows.md). Change the steps here only by changing the
7+
# Makefile.
8+
39
on:
410
push:
511
branches: ["main", "copilot/**"]
612
pull_request:
713
branches: ["main"]
814
workflow_call:
915

16+
# Least-privilege by default; jobs needing more declare it explicitly.
17+
permissions:
18+
contents: read
19+
20+
# Cancel superseded runs on the same ref so a new push to a PR stops the
21+
# previous run instead of burning runner time. Keyed on the ref so distinct
22+
# branches/PRs stay independent.
23+
concurrency:
24+
group: ci-${{ github.workflow }}-${{ github.ref }}
25+
cancel-in-progress: true
26+
1027
jobs:
1128
test:
1229
name: "Python ${{ matrix.python-version }}"
@@ -18,64 +35,162 @@ jobs:
1835
python-version: ["3.10", "3.11", "3.12"]
1936

2037
steps:
21-
- uses: actions/checkout@v4
38+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
2239

2340
- name: Set up Python ${{ matrix.python-version }}
24-
uses: actions/setup-python@v5
41+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
2542
with:
2643
python-version: ${{ matrix.python-version }}
44+
cache: pip
45+
cache-dependency-path: pyproject.toml
2746

2847
- name: Install dependencies
2948
run: pip install -e ".[dev]"
3049

31-
- name: Lint (ruff check)
32-
run: ruff check src/ tests/ examples/
50+
# Each step is a Makefile target — see the contract note at the top.
51+
- name: Format check
52+
run: make fmt-check
3353

34-
- name: Format check (ruff format)
35-
run: ruff format --check src/ tests/ examples/
54+
- name: Lint
55+
run: make lint
3656

37-
- name: Type check (mypy)
38-
run: mypy src/
57+
- name: Type check
58+
run: make type
3959

40-
- name: Test (pytest)
41-
run: python -m pytest -q --cov=weaver_kernel --cov-report=term-missing
60+
- name: Test
61+
run: make test
4262

4363
- name: Examples
44-
run: |
45-
python examples/basic_cli.py
46-
python examples/billing_demo.py
47-
python examples/http_driver_demo.py
48-
python examples/tutorial.py
49-
python examples/readme_quickstart.py
50-
python examples/trace_export_demo.py
51-
python examples/ocsf_export_demo.py
52-
python examples/trace_replay_demo.py
53-
54-
conformance_stub:
55-
name: "Weaver Spec Conformance Stub (v0.1.0)"
64+
run: make example
65+
66+
- name: Coverage HTML report
67+
if: always()
68+
run: python -m coverage html
69+
70+
- name: Upload coverage HTML
71+
if: always()
72+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
73+
with:
74+
name: coverage-html-${{ matrix.python-version }}
75+
path: htmlcov/
76+
if-no-files-found: ignore
77+
78+
bare-install:
79+
name: "Bare install (no extras)"
5680
runs-on: ubuntu-latest
5781
needs: test
5882
permissions:
5983
contents: read
6084

6185
steps:
62-
- uses: actions/checkout@v4
86+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
6387

6488
- name: Set up Python
65-
uses: actions/setup-python@v5
89+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
6690
with:
6791
python-version: "3.12"
92+
cache: pip
93+
cache-dependency-path: pyproject.toml
6894

69-
- name: Install dependencies
70-
run: pip install -e ".[dev]"
95+
# No extras: proves the "minimal deps (httpx + pydantic)" claim holds.
96+
- name: Install (no extras)
97+
run: pip install .
98+
99+
- name: Import the full public API
100+
run: |
101+
python -c "import weaver_kernel as w; \
102+
missing = [n for n in w.__all__ if not hasattr(w, n)]; \
103+
assert not missing, f'missing public symbols: {missing}'; \
104+
print(f'imported {len(w.__all__)} public symbols')"
105+
106+
- name: Run the README quickstart
107+
run: python examples/readme_quickstart.py
108+
109+
- name: Assert optional extras are genuinely absent
110+
run: |
111+
for mod in mcp yaml opentelemetry tiktoken weaver_contracts; do
112+
if python -c "import $mod" 2>/dev/null; then
113+
echo "::error::optional dependency '$mod' is importable in a bare install"
114+
exit 1
115+
fi
116+
done
117+
echo "no optional extras leaked into the base install"
71118
72-
# Placeholder: activate once dgenio/weaver-spec#4 ships the conformance runner.
73-
# weaver-spec and weaver-contracts are published on PyPI.
74-
# weaver_contracts.conformance does not yet exist (dgenio/weaver-spec#4).
75-
# Replace this step with:
76-
# pip install weaver-contracts # PyPI dist name uses a hyphen
77-
# python -m weaver_contracts.conformance --target weaver_kernel
78-
- name: weaver-spec conformance suite (stub)
119+
- name: Assert the MCP-extra-missing error is helpful
79120
run: |
80-
echo "weaver-contracts 0.2.0 is on PyPI; weaver_contracts.conformance runner not yet available (dgenio/weaver-spec#4)."
81-
echo "Stub passes. Activate when dgenio/weaver-spec#4 ships."
121+
python - <<'PY'
122+
from weaver_kernel.drivers.mcp_support import import_optional
123+
try:
124+
import_optional("mcp.client.session")
125+
except ImportError as exc:
126+
assert "weaver-kernel[mcp]" in str(exc), f"unhelpful error: {exc}"
127+
print("MCP-extra-missing error is documented and actionable")
128+
else:
129+
raise AssertionError("expected ImportError without the mcp extra")
130+
PY
131+
132+
security-audit:
133+
name: "Dependency audit (pip-audit)"
134+
runs-on: ubuntu-latest
135+
permissions:
136+
contents: read
137+
138+
steps:
139+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
140+
141+
- name: Set up Python
142+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
143+
with:
144+
python-version: "3.12"
145+
cache: pip
146+
cache-dependency-path: pyproject.toml
147+
148+
- name: Install pip-audit
149+
run: pip install pip-audit
150+
151+
# Resolve weaver-kernel's *runtime* dependency tree in an isolated venv
152+
# (no extras, no pip-audit) and audit exactly that, so pip-audit's own
153+
# dependencies can never cause a failure unrelated to what adopters ship.
154+
- name: Resolve runtime dependency tree
155+
run: |
156+
python -m venv /tmp/runtime
157+
/tmp/runtime/bin/pip install .
158+
/tmp/runtime/bin/pip freeze --exclude-editable \
159+
| grep -viE '^(weaver-kernel|pip|setuptools)([=@ ]|$)' > runtime-requirements.txt
160+
echo "Auditing:"; cat runtime-requirements.txt
161+
162+
# Policy: fail on any known vulnerability in the runtime tree. Document a
163+
# false positive by appending `--ignore-vuln <ID>` here with a comment
164+
# (see README "Security automation").
165+
- name: Audit runtime dependencies
166+
run: pip-audit --strict --desc --requirement runtime-requirements.txt
167+
168+
conformance:
169+
name: "Weaver-spec conformance"
170+
runs-on: ubuntu-latest
171+
needs: test
172+
permissions:
173+
contents: read
174+
175+
steps:
176+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
177+
178+
- name: Set up Python
179+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
180+
with:
181+
python-version: "3.12"
182+
cache: pip
183+
cache-dependency-path: pyproject.toml
184+
185+
# Real validation (no echo): map kernel Frame/ActionTrace/token onto the
186+
# published weaver-contracts dataclasses and assert they validate. When
187+
# dgenio/weaver-spec#4 ships weaver_contracts.conformance, add its runner
188+
# here as an additional step.
189+
- name: Install conformance extra
190+
run: pip install ".[conformance]" pytest pytest-asyncio
191+
192+
- name: Report contract version
193+
run: python -c "from weaver_kernel.conformance import contract_version; print('weaver-contracts', contract_version())"
194+
195+
- name: Run conformance mapping tests
196+
run: python -m pytest tests/test_conformance.py -q

.github/workflows/codeql.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: CodeQL
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["main"]
8+
schedule:
9+
# Weekly, so newly-published advisories are caught even without a push.
10+
- cron: "27 3 * * 1"
11+
12+
permissions:
13+
contents: read
14+
15+
concurrency:
16+
group: codeql-${{ github.workflow }}-${{ github.ref }}
17+
cancel-in-progress: true
18+
19+
jobs:
20+
analyze:
21+
name: "Analyze (python)"
22+
runs-on: ubuntu-latest
23+
permissions:
24+
contents: read
25+
security-events: write # required to upload CodeQL results to the Security tab
26+
27+
steps:
28+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
29+
30+
- name: Initialize CodeQL
31+
uses: github/codeql-action/init@8272c299f21ca24af15dfe9ac0971ba969e5e0d5 # v3.36.2
32+
with:
33+
languages: python
34+
queries: security-and-quality
35+
36+
# Python is interpreted — no build step is needed, so autobuild is omitted.
37+
- name: Perform CodeQL Analysis
38+
uses: github/codeql-action/analyze@8272c299f21ca24af15dfe9ac0971ba969e5e0d5 # v3.36.2
39+
with:
40+
category: "/language:python"

.github/workflows/publish.yml

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,33 @@ jobs:
2929
- name: Build sdist and wheel
3030
run: python -m build
3131

32+
# Describe the *published package's* runtime tree (not the build env): a
33+
# clean venv with only the built wheel installed, introspected by
34+
# cyclonedx-py from a separate tool install so the tool's own deps do not
35+
# pollute the SBOM. Written to sbom/ (NOT dist/) so it is never uploaded
36+
# to PyPI as a distribution.
37+
- name: Generate SBOM (CycloneDX)
38+
run: |
39+
python -m pip install cyclonedx-bom
40+
python -m venv /tmp/wheelenv
41+
/tmp/wheelenv/bin/pip install dist/*.whl
42+
mkdir -p sbom
43+
cyclonedx-py environment /tmp/wheelenv/bin/python \
44+
--of JSON --mc-type library --pyproject pyproject.toml \
45+
-o sbom/weaver-kernel.cdx.json
46+
3247
- name: Upload dist artifacts
3348
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
3449
with:
3550
name: dist
3651
path: dist/
3752

53+
- name: Upload SBOM artifact
54+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
55+
with:
56+
name: sbom
57+
path: sbom/
58+
3859
release:
3960
name: "GitHub Release"
4061
needs: build
@@ -48,12 +69,20 @@ jobs:
4869
name: dist
4970
path: dist/
5071

72+
- name: Download SBOM artifact
73+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
74+
with:
75+
name: sbom
76+
path: sbom/
77+
5178
- name: Create GitHub Release
5279
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
5380
with:
5481
generate_release_notes: true
5582
fail_on_unmatched_files: true
56-
files: dist/*
83+
files: |
84+
dist/*
85+
sbom/*
5786
5887
publish:
5988
name: "Publish to PyPI"
@@ -72,3 +101,8 @@ jobs:
72101

73102
- name: Publish to PyPI
74103
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
104+
with:
105+
# PEP 740 attestations, signed via the Trusted Publisher OIDC identity
106+
# (the id-token: write permission above). Verifiable on the PyPI
107+
# project page and with the `pypi-attestations` CLI — see RELEASE.md.
108+
attestations: true

0 commit comments

Comments
 (0)