Skip to content

Commit 75c25e6

Browse files
claudedevonartis
authored andcommitted
v0.3.0 — AgentWrit Python SDK public release
Merge develop → main. First public release. SDK: complete rewrite with create_agent() → Agent lifecycle, Ed25519 challenge-response, typed models, RFC 7807 errors. Testing: 99 unit + 15 acceptance, 11 CI gates. Demos: MedAssist AI + Support Tickets, Docker images on Hub. PyPI: pip install agentwrit (v0.3.0 live). CI: auto-publish PyPI on tag, auto-push Docker on main. Closes devonartis/agentwrit#31 Closes devonartis/agentwrit#33 Generated with Claude Code Harness Agent Co-Authored-By: Claude <claude@anthropic.com>
2 parents a249c7c + ba2902a commit 75c25e6

141 files changed

Lines changed: 19594 additions & 5204 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
name: Branch Cleanup
2+
3+
# Only runs after CI passes on a push to develop or main.
4+
# Deletes the source branch of the merged PR — but ONLY if post-merge CI succeeded.
5+
# If CI fails after merge, the branch stays so you can fix and re-merge.
6+
7+
on:
8+
workflow_run:
9+
workflows: ["CI"]
10+
types: [completed]
11+
branches: [develop, main]
12+
13+
permissions:
14+
contents: write
15+
pull-requests: read
16+
17+
jobs:
18+
cleanup:
19+
name: Delete merged branch
20+
runs-on: ubuntu-latest
21+
if: >-
22+
github.event.workflow_run.conclusion == 'success' &&
23+
github.event.workflow_run.event == 'push'
24+
steps:
25+
- name: Find merged PR and delete source branch
26+
uses: actions/github-script@v7
27+
with:
28+
script: |
29+
const sha = context.payload.workflow_run.head_sha;
30+
const repo = context.repo;
31+
32+
// Find PRs that were merged with this commit
33+
const { data: prs } = await github.rest.pulls.list({
34+
...repo,
35+
state: 'closed',
36+
sort: 'updated',
37+
direction: 'desc',
38+
per_page: 10
39+
});
40+
41+
const merged = prs.filter(pr =>
42+
pr.merged_at && pr.merge_commit_sha === sha
43+
);
44+
45+
if (merged.length === 0) {
46+
console.log('No merged PR found for this push — nothing to clean up.');
47+
return;
48+
}
49+
50+
for (const pr of merged) {
51+
const branch = pr.head.ref;
52+
const base = pr.base.ref;
53+
54+
// Never delete develop or main
55+
if (['develop', 'main', 'master'].includes(branch)) {
56+
console.log(`Skipping protected branch: ${branch}`);
57+
continue;
58+
}
59+
60+
// Only delete if the branch is in this repo (not a fork)
61+
if (pr.head.repo.full_name !== repo.owner + '/' + repo.repo) {
62+
console.log(`Skipping fork branch: ${pr.head.repo.full_name}/${branch}`);
63+
continue;
64+
}
65+
66+
try {
67+
await github.rest.git.deleteRef({
68+
...repo,
69+
ref: `heads/${branch}`
70+
});
71+
console.log(`Deleted branch ${branch} (PR #${pr.number} merged to ${base}, post-merge CI passed)`);
72+
} catch (e) {
73+
if (e.status === 422) {
74+
console.log(`Branch ${branch} already deleted`);
75+
} else {
76+
throw e;
77+
}
78+
}
79+
}
80+
81+
- name: Report if CI failed (branch preserved)
82+
if: github.event.workflow_run.conclusion == 'failure'
83+
run: |
84+
echo "⚠️ Post-merge CI FAILED — source branch NOT deleted."
85+
echo "Fix the issue, then the branch will be cleaned up on the next successful run."

.github/workflows/ci.yml

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [develop, main]
6+
pull_request:
7+
branches: [develop, main]
8+
9+
permissions:
10+
contents: read
11+
pull-requests: read
12+
13+
jobs:
14+
lint:
15+
name: Lint
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: astral-sh/setup-uv@v4
20+
with:
21+
version: "latest"
22+
- run: uv sync --all-extras
23+
- run: uv run ruff check .
24+
25+
typecheck:
26+
name: Type Check
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@v4
30+
- uses: astral-sh/setup-uv@v4
31+
with:
32+
version: "latest"
33+
- run: uv sync --all-extras
34+
- run: uv run mypy --strict src/
35+
36+
unit-tests:
37+
name: Unit Tests
38+
runs-on: ubuntu-latest
39+
strategy:
40+
matrix:
41+
python-version: ["3.10", "3.11", "3.12", "3.13"]
42+
steps:
43+
- uses: actions/checkout@v4
44+
- uses: astral-sh/setup-uv@v4
45+
with:
46+
version: "latest"
47+
- run: uv python install ${{ matrix.python-version }}
48+
- run: uv sync --all-extras --python ${{ matrix.python-version }}
49+
- run: uv run pytest tests/unit/ -q
50+
51+
integration-tests:
52+
name: Integration Tests
53+
runs-on: ubuntu-latest
54+
services:
55+
broker:
56+
image: devonartis/agentwrit:latest
57+
ports:
58+
- 8080:8080
59+
env:
60+
AA_PORT: "8080"
61+
AA_BIND_ADDRESS: "0.0.0.0"
62+
# Broker binary uses AA_ prefix (devonartis/agentwrit#44)
63+
AA_ADMIN_SECRET: ${{ secrets.AGENTWRIT_ADMIN_SECRET }}
64+
options: >-
65+
--health-cmd "wget --spider -q http://localhost:8080/v1/health"
66+
--health-interval 2s
67+
--health-timeout 3s
68+
--health-retries 10
69+
steps:
70+
- uses: actions/checkout@v4
71+
- uses: astral-sh/setup-uv@v4
72+
with:
73+
version: "latest"
74+
- run: uv sync --all-extras
75+
- name: Register test app with broker
76+
id: register-app
77+
env:
78+
AGENTWRIT_BROKER_URL: http://localhost:8080
79+
AA_ADMIN_SECRET: ${{ secrets.AGENTWRIT_ADMIN_SECRET }}
80+
run: |
81+
# Authenticate as admin
82+
ADMIN_TOKEN=$(curl -sf -X POST "${AGENTWRIT_BROKER_URL}/v1/admin/auth" \
83+
-H "Content-Type: application/json" \
84+
-d "{\"secret\":\"${AA_ADMIN_SECRET}\"}" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
85+
86+
# Register a test app with broad scope ceiling
87+
APP_JSON=$(curl -sf -X POST "${AGENTWRIT_BROKER_URL}/v1/admin/apps" \
88+
-H "Content-Type: application/json" \
89+
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
90+
-d '{"name":"ci-integration","scopes":["read:data:*","write:data:*"]}')
91+
92+
# Extract credentials and mask the secret
93+
CLIENT_ID=$(echo "${APP_JSON}" | python3 -c "import sys,json; print(json.load(sys.stdin)['client_id'])")
94+
CLIENT_SECRET=$(echo "${APP_JSON}" | python3 -c "import sys,json; print(json.load(sys.stdin)['client_secret'])")
95+
96+
echo "::add-mask::${CLIENT_SECRET}"
97+
echo "client_id=${CLIENT_ID}" >> "$GITHUB_OUTPUT"
98+
echo "client_secret=${CLIENT_SECRET}" >> "$GITHUB_OUTPUT"
99+
echo "Registered test app: ${CLIENT_ID}"
100+
- name: Run integration tests (all 15 stories)
101+
env:
102+
AGENTWRIT_BROKER_URL: http://localhost:8080
103+
AGENTWRIT_ADMIN_SECRET: ${{ secrets.AGENTWRIT_ADMIN_SECRET }}
104+
AGENTWRIT_CLIENT_ID: ${{ steps.register-app.outputs.client_id }}
105+
AGENTWRIT_CLIENT_SECRET: ${{ steps.register-app.outputs.client_secret }}
106+
run: |
107+
result=$(uv run pytest -m integration -v 2>&1)
108+
echo "${result}"
109+
110+
# Fail if any tests were skipped — no silent skips allowed
111+
if echo "${result}" | grep -q "skipped"; then
112+
echo "::error::Integration tests had skipped stories — all 15 must run"
113+
exit 1
114+
fi
115+
116+
sast:
117+
name: SAST (bandit)
118+
runs-on: ubuntu-latest
119+
steps:
120+
- uses: actions/checkout@v4
121+
- uses: astral-sh/setup-uv@v4
122+
with:
123+
version: "latest"
124+
- run: uv sync --all-extras
125+
- run: uv run bandit -r src/ -q
126+
127+
dep-audit:
128+
name: Dependency Audit (pip-audit)
129+
runs-on: ubuntu-latest
130+
steps:
131+
- uses: actions/checkout@v4
132+
- uses: astral-sh/setup-uv@v4
133+
with:
134+
version: "latest"
135+
- run: uv sync --all-extras
136+
- run: uv run pip-audit
137+
138+
secrets-scan:
139+
name: Secrets Scan
140+
runs-on: ubuntu-latest
141+
steps:
142+
- uses: actions/checkout@v4
143+
with:
144+
fetch-depth: 0
145+
- uses: gitleaks/gitleaks-action@v2
146+
env:
147+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
148+
149+
no-ignored-tracked:
150+
name: No Ignored Files Tracked
151+
runs-on: ubuntu-latest
152+
steps:
153+
- uses: actions/checkout@v4
154+
- name: Check for tracked files that should be ignored
155+
run: |
156+
tracked_ignored=$(git ls-files -i --exclude-standard 2>/dev/null || true)
157+
if [ -n "$tracked_ignored" ]; then
158+
echo "ERROR: These tracked files are in .gitignore:"
159+
echo "$tracked_ignored"
160+
exit 1
161+
fi
162+
echo "Clean — no tracked files match .gitignore patterns."

.github/workflows/docker-demos.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Build & Push Demo Images
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "demo/**"
8+
- "demo2/**"
9+
- "src/**"
10+
- "pyproject.toml"
11+
- "uv.lock"
12+
workflow_dispatch:
13+
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
build-medassist:
19+
name: Build & Push MedAssist
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
24+
- name: Set up Docker Buildx
25+
uses: docker/setup-buildx-action@v3
26+
27+
- name: Log in to Docker Hub
28+
uses: docker/login-action@v3
29+
with:
30+
username: ${{ secrets.DOCKERHUB_USERNAME }}
31+
password: ${{ secrets.DOCKERHUB_TOKEN }}
32+
33+
- name: Extract version from pyproject.toml
34+
id: version
35+
run: |
36+
VERSION=$(grep '^version' pyproject.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
37+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
38+
39+
- name: Build and push
40+
uses: docker/build-push-action@v6
41+
with:
42+
context: .
43+
file: demo/Dockerfile
44+
push: true
45+
tags: |
46+
devonartis/agentwrit-medassist:latest
47+
devonartis/agentwrit-medassist:${{ steps.version.outputs.version }}
48+
cache-from: type=gha
49+
cache-to: type=gha,mode=max
50+
51+
build-support-tickets:
52+
name: Build & Push Support Tickets
53+
runs-on: ubuntu-latest
54+
steps:
55+
- uses: actions/checkout@v4
56+
57+
- name: Set up Docker Buildx
58+
uses: docker/setup-buildx-action@v3
59+
60+
- name: Log in to Docker Hub
61+
uses: docker/login-action@v3
62+
with:
63+
username: ${{ secrets.DOCKERHUB_USERNAME }}
64+
password: ${{ secrets.DOCKERHUB_TOKEN }}
65+
66+
- name: Extract version from pyproject.toml
67+
id: version
68+
run: |
69+
VERSION=$(grep '^version' pyproject.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
70+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
71+
72+
- name: Build and push
73+
uses: docker/build-push-action@v6
74+
with:
75+
context: .
76+
file: demo2/Dockerfile
77+
push: true
78+
tags: |
79+
devonartis/agentwrit-support-tickets:latest
80+
devonartis/agentwrit-support-tickets:${{ steps.version.outputs.version }}
81+
cache-from: type=gha
82+
cache-to: type=gha,mode=max
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Build & Push MedAssist Demo
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "demo/**"
8+
- "src/**"
9+
- "pyproject.toml"
10+
- "uv.lock"
11+
workflow_dispatch:
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
build-and-push:
18+
name: Build & Push Docker Image
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Set up Docker Buildx
24+
uses: docker/setup-buildx-action@v3
25+
26+
- name: Log in to Docker Hub
27+
uses: docker/login-action@v3
28+
with:
29+
username: ${{ secrets.DOCKERHUB_USERNAME }}
30+
password: ${{ secrets.DOCKERHUB_TOKEN }}
31+
32+
- name: Extract version from pyproject.toml
33+
id: version
34+
run: |
35+
VERSION=$(grep '^version' pyproject.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
36+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
37+
echo "Version: ${VERSION}"
38+
39+
- name: Build and push
40+
uses: docker/build-push-action@v6
41+
with:
42+
context: .
43+
file: demo/Dockerfile
44+
push: true
45+
tags: |
46+
devonartis/agentwrit-medassist:latest
47+
devonartis/agentwrit-medassist:${{ steps.version.outputs.version }}
48+
cache-from: type=gha
49+
cache-to: type=gha,mode=max

0 commit comments

Comments
 (0)