Skip to content

Commit e542435

Browse files
authored
Merge pull request #108 from constk/release/0.2.17
release: bring main up to develop (0.2.17 — release-readiness docs + eval pattern examples + transitive CVE patches)
2 parents eff5b1c + 2028329 commit e542435

18 files changed

Lines changed: 827 additions & 58 deletions

.github/scripts/check_pin_freshness.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,22 @@ def _fetch_json(url: str, token: str) -> dict[str, object] | None:
110110
return payload if isinstance(payload, dict) else None
111111

112112

113+
def _action_repo(action: str) -> str:
114+
"""Return `owner/repo` for an action string that may carry a sub-path.
115+
116+
Action references can be `owner/repo` or `owner/repo/path/to/subaction`
117+
(e.g. `github/codeql-action/init`). Only the first two slash-segments
118+
name the GitHub repository — the trailing segments are paths within
119+
the repo's tree (containing per-subaction `action.yml` files). The
120+
REST API endpoint we hit (`/repos/<owner>/<repo>/git/...`) only
121+
accepts the `owner/repo` form; passing the full action string would
122+
404 on every sub-path action and surface as a false-positive
123+
"tag no longer resolves" finding.
124+
"""
125+
parts = action.split("/", 2)
126+
return "/".join(parts[:2]) if len(parts) >= 2 else action
127+
128+
113129
def _resolve_tag_sha(action: str, tag: str, token: str) -> str | None:
114130
"""Return the commit SHA the tag points at, or None on missing/error.
115131
@@ -118,7 +134,8 @@ def _resolve_tag_sha(action: str, tag: str, token: str) -> str | None:
118134
commit. Lightweight tags resolve in one GET (the ref's `object.sha`
119135
is the commit directly).
120136
"""
121-
ref = _fetch_json(f"{_API_BASE}/repos/{action}/git/refs/tags/{tag}", token)
137+
repo = _action_repo(action)
138+
ref = _fetch_json(f"{_API_BASE}/repos/{repo}/git/refs/tags/{tag}", token)
122139
if ref is None:
123140
return None
124141
obj = ref.get("object")
@@ -132,7 +149,7 @@ def _resolve_tag_sha(action: str, tag: str, token: str) -> str | None:
132149
return obj_sha
133150
if obj_type == "tag":
134151
# Annotated tag — dereference to the commit it points at.
135-
annotated = _fetch_json(f"{_API_BASE}/repos/{action}/git/tags/{obj_sha}", token)
152+
annotated = _fetch_json(f"{_API_BASE}/repos/{repo}/git/tags/{obj_sha}", token)
136153
if annotated is None:
137154
return None
138155
inner = annotated.get("object")

.github/workflows/ci.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
runs-on: ubuntu-latest
1919
steps:
2020
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
21-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
21+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
2222
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
2323
with:
2424
python-version: "3.14"
@@ -31,7 +31,7 @@ jobs:
3131
runs-on: ubuntu-latest
3232
steps:
3333
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
34-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
34+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
3535
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
3636
with:
3737
python-version: "3.14"
@@ -44,7 +44,7 @@ jobs:
4444
# Pure in-process tests — completes fast so PR authors get quick feedback.
4545
steps:
4646
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
47-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
47+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
4848
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
4949
with:
5050
python-version: "3.14"
@@ -57,7 +57,7 @@ jobs:
5757
# Enforces [tool.coverage.report].fail_under from pyproject.toml (75%).
5858
steps:
5959
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
60-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
60+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
6161
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
6262
with:
6363
python-version: "3.14"
@@ -69,7 +69,7 @@ jobs:
6969
runs-on: ubuntu-latest
7070
steps:
7171
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
72-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
72+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
7373
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
7474
with:
7575
python-version: "3.14"
@@ -84,7 +84,7 @@ jobs:
8484
# secret past the first defence layer.
8585
steps:
8686
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
87-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
87+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
8888
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
8989
with:
9090
python-version: "3.14"
@@ -218,7 +218,7 @@ jobs:
218218
# actual workflow jobs on disk.
219219
steps:
220220
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
221-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
221+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
222222
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
223223
with:
224224
python-version: "3.14"
@@ -234,7 +234,7 @@ jobs:
234234
# while PR titles fail in CI (or vice versa).
235235
steps:
236236
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
237-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
237+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
238238
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
239239
with:
240240
python-version: "3.14"

.github/workflows/codeql.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ jobs:
4444
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
4545

4646
- name: Initialize CodeQL
47-
uses: github/codeql-action/init@v3
47+
uses: github/codeql-action/init@v4
4848
with:
4949
languages: ${{ matrix.language }}
5050
build-mode: ${{ matrix.build-mode }}
5151

5252
- name: Perform CodeQL Analysis
53-
uses: github/codeql-action/analyze@v3
53+
uses: github/codeql-action/analyze@v4
5454
with:
5555
category: "/language:${{ matrix.language }}"

.github/workflows/eval-nightly.yml

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
# Eval harness nightly — disabled-by-default.
22
#
3-
# This workflow runs the golden QA dataset against the agent / LLM loop. It
4-
# is `workflow_dispatch`-only by default to prevent accidental LLM API
5-
# spend. To enable nightly runs:
3+
# This workflow runs the golden QA dataset + worked-pattern cases against a
4+
# real Azure OpenAI deployment. It is `workflow_dispatch`-only by default
5+
# to prevent accidental API spend. To enable nightly runs:
6+
#
7+
# 1. Set the Azure OpenAI secrets in repo settings:
8+
# AZURE_OPENAI_ENDPOINT e.g. https://my.openai.azure.com
9+
# AZURE_OPENAI_API_KEY the Azure resource key
10+
# AZURE_OPENAI_DEPLOYMENT deployment name, e.g. gpt-4o-mini
11+
# AZURE_OPENAI_API_VERSION optional, defaults to 2024-10-21
612
#
7-
# 1. Set the LLM secrets in repo settings (LLM_API_KEY at minimum;
8-
# LLM_BASE_URL / LLM_MODEL / LLM_PROVIDER if your judge differs from
9-
# OpenAI defaults).
1013
# 2. Replace the `on:` block below with:
1114
#
1215
# on:
1316
# schedule:
1417
# - cron: "0 6 * * *" # daily 06:00 UTC
1518
# workflow_dispatch:
1619
#
17-
# 3. Add the `eval-nightly.yml` to EXEMPT_WORKFLOWS in
18-
# `.github/scripts/check_required_contexts.py` if it's not already
19-
# there (it is, by default — scheduled runs never gate PRs).
20+
# 3. Confirm `eval-nightly.yml` is in EXEMPT_WORKFLOWS in
21+
# `.github/scripts/check_required_contexts.py` (it is, by default
22+
# — scheduled runs never gate PRs).
23+
#
24+
# When the Azure secrets are absent, eval/test_golden_patterns.py is
25+
# skipped via pytestmark — the toy eval/test_golden_qa.py case still
26+
# runs as a smoke check on the runner mechanics.
2027
#
2128
# See docs/EVAL_HARNESS.md for the full setup story.
2229

@@ -39,15 +46,15 @@ jobs:
3946
runs-on: ubuntu-latest
4047
steps:
4148
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
42-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
49+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
4350
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
4451
with:
4552
python-version: ${{ inputs.python_version || '3.14' }}
46-
- run: uv sync --frozen --extra dev
53+
- run: uv sync --frozen --extra dev --extra eval
4754
- name: Run pytest eval/
4855
env:
49-
LLM_PROVIDER: ${{ secrets.LLM_PROVIDER }}
50-
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
51-
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
52-
LLM_MODEL: ${{ secrets.LLM_MODEL }}
56+
AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
57+
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
58+
AZURE_OPENAI_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_DEPLOYMENT }}
59+
AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }}
5360
run: uv run pytest eval/ -v

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
# annotation when a new release lands and you've reviewed the diff.
3030
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
3131

32-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
32+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
3333

3434
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
3535
with:

.github/workflows/security.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
runs-on: ubuntu-latest
4545
steps:
4646
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
47-
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
47+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
4848
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
4949
with:
5050
python-version: "3.14"

CONTRIBUTING.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,39 @@ The subject is **lowercase** after the colon. Title Case prose (`Add the thing`)
4646

4747
### Solo-owner merge policy
4848

49-
This repo runs with a single code owner (`* @constk` in `CODEOWNERS`). GitHub forbids a PR author from approving their own PR, so the standard "1 code-owner review" gate cannot be satisfied without an admin override. While in this state, the **intended workflow is**:
49+
> **Transitional — only while this repo has a single code owner.** Standard practice is a code-owner review on every PR. The flow below exists because GitHub forbids self-approval, so a single-owner repo cannot satisfy the "1 code-owner review" gate any other way. The exemption is **removed** the moment a second collaborator with merge rights joins.
50+
51+
This repo currently runs with a single code owner (`* @constk` in `CODEOWNERS`). While in this state, the intended merge command is:
5052

5153
```sh
5254
gh pr merge <N> --admin --squash --delete-branch
5355
```
5456

55-
…for `feat:` / `fix:` / `chore:` PRs, and `--admin --merge` (preserves history) for `release:` PRs. The `enforce_admins: false` line in `.github/branch-protection/{develop,main}.json` is the documented escape hatch — admin merge here is the policy, not a deviation from it.
57+
…for `feat:` / `fix:` / `chore:` PRs, and `--admin --merge` (preserves history) for `release:` PRs. The `enforce_admins: false` line in `.github/branch-protection/{develop,main}.json` is the documented escape hatch — admin merge here is the documented single-owner workaround, not bypass of the gates (every required status check still has to pass).
58+
59+
**When the exemption ends.** As soon as a second collaborator with merge rights is onboarded:
60+
61+
1. Drop the `--admin` flag from the merge command and adopt standard PR review.
62+
2. Remove this entire subsection.
63+
3. Update `CODEOWNERS` to add the new collaborator.
64+
4. Flip `enforce_admins` to `true` in the branch-protection JSON for both branches. Leaving it `false` would keep the admin-bypass door open even after the single-owner workaround is no longer needed — defeats the point of removing the workaround.
65+
66+
All four changes land in a single PR.
67+
68+
## Line endings (Windows clones)
69+
70+
This repo enforces LF line endings via `.gitattributes` (`* text=auto eol=lf`)
71+
and the pre-commit hygiene hook. If you cloned on Windows with
72+
`core.autocrlf=true`, the first checkout after pulling the `.gitattributes`
73+
change can leave the working tree out of sync with the index. Renormalise
74+
once:
75+
76+
```sh
77+
git add --renormalize .
78+
git commit -m "chore: renormalise line endings"
79+
```
5680

57-
When a second collaborator joins, drop the `--admin` flag and adopt standard PR review. Update this section + `CODEOWNERS` in the same PR.
81+
After that, day-to-day work is unaffected.
5882

5983
## Line endings (Windows clones)
6084

README.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
[![React 19.2](https://img.shields.io/badge/react-19.2-61dafb.svg)](https://react.dev/)
99
[![Coverage 98%](https://img.shields.io/badge/coverage-98%25-brightgreen.svg)](docs/HARNESS.md)
1010

11-
> A production-quality coding harness for Python (FastAPI) + Vite/React/TypeScript projects. Designed for LLM-driven development: every gate lint, types, architecture, security, eval — is enforced mechanically so code quality stays consistent across many human and AI contributors.
11+
> Production-grade SDLC harness for human–LLM coding collaborations — keeping quality consistent regardless of who shipped the code. Python (FastAPI) + Vite/React/TypeScript, with every gate (lint, types, architecture, security, eval) enforced mechanically in CI, not by discipline.
1212
1313
## What ships
1414

@@ -81,23 +81,31 @@ The scaffold's React page hits `/api/v1/health` on load and renders the version
8181

8282
![Hello page](docs/images/hello-page.png)
8383

84+
### Jaeger trace (`docker compose up` + `/api/v1/health`)
85+
86+
The full stack — backend, frontend, Jaeger collector — boots with `docker compose up`. Hitting `/api/v1/health` once produces an OpenTelemetry trace exported via OTLP/gRPC; the span hierarchy is visible at <http://localhost:16686> under the `harness-python-react` service, with `agent_span(...)` attributes attached using only the keys constant-defined at the top of [`src/observability/spans.py`](src/observability/spans.py).
87+
8488
<!--
85-
TODO (#28): one capture left — Jaeger trace.
89+
Screenshot pending: docs/images/jaeger-trace.png
8690
87-
docs/images/jaeger-trace.png
88-
With the full stack running (`docker compose up`), hit /api/v1/health
89-
once, then open http://localhost:16686, select service
90-
`harness-python-react`, click the most recent trace, screenshot the
91-
span timeline.
91+
Capture recipe (run once and commit the PNG to docs/images/):
92+
1. docker compose up
93+
2. curl http://localhost:8000/api/v1/health
94+
3. open http://localhost:16686 -> select service "harness-python-react"
95+
4. click the most recent trace
96+
5. screenshot the span timeline, save as docs/images/jaeger-trace.png
9297
93-
When the PNG lands in docs/images/, replace this comment with a section
94-
analogous to "Hello page" above.
98+
When the PNG is committed, replace this whole comment with:
99+
100+
![Jaeger trace — span timeline for GET /api/v1/health](docs/images/jaeger-trace.png)
95101
-->
96102

97103
## Why a harness
98104

99105
The differentiator isn't the scaffold — it's that every layer of the pipeline catches a different failure class **without relying on the human or LLM coder remembering to run anything**. The same posture protects code regardless of who wrote it.
100106

107+
> **Example.** An agent added `from src.tools import ...` inside `src.models` for type reuse. `lint-imports` failed CI — the `src.models depends on nothing in src/` contract broke — and pointed the next iteration at [`docs/BOUNDARIES.md`](docs/BOUNDARIES.md). The type moved into `src.models` instead. Never shipped.
108+
101109
See [`docs/HARNESS.md`](docs/HARNESS.md) for the full umbrella. Highlights:
102110

103111
- **Pydantic `StrictModel` everywhere a contract crosses a seam** (rejects unknown keys at construction).

0 commit comments

Comments
 (0)