openaidr 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openaidr-0.0.1/.claude/settings.json +35 -0
- openaidr-0.0.1/.claude/skills/release-openaidr/SKILL.md +277 -0
- openaidr-0.0.1/.github/workflows/autofix.yml +332 -0
- openaidr-0.0.1/.github/workflows/ci.yml +62 -0
- openaidr-0.0.1/.github/workflows/claude.yml +162 -0
- openaidr-0.0.1/.github/workflows/publish-pypi.yml +178 -0
- openaidr-0.0.1/.gitignore +14 -0
- openaidr-0.0.1/AGENTS.md +1 -0
- openaidr-0.0.1/CLAUDE.md +246 -0
- openaidr-0.0.1/LICENSE +202 -0
- openaidr-0.0.1/NOTICE.md +7 -0
- openaidr-0.0.1/PKG-INFO +70 -0
- openaidr-0.0.1/README.md +48 -0
- openaidr-0.0.1/docs/adrs/HOOK-PROMPT.md +1 -0
- openaidr-0.0.1/docs/adrs/INDEX.md +28 -0
- openaidr-0.0.1/docs/adrs/TEMPLATE.md +42 -0
- openaidr-0.0.1/docs/releases/README.md +14 -0
- openaidr-0.0.1/docs/releases/v0.0.1.md +31 -0
- openaidr-0.0.1/pyproject.toml +53 -0
- openaidr-0.0.1/scripts/git-hooks/pre-push +108 -0
- openaidr-0.0.1/scripts/install-hooks.sh +36 -0
- openaidr-0.0.1/src/openaidr/__init__.py +8 -0
- openaidr-0.0.1/src/openaidr/__main__.py +25 -0
- openaidr-0.0.1/tests/test_cli.py +37 -0
- openaidr-0.0.1/tests/test_placeholder.py +7 -0
- openaidr-0.0.1/uv.lock +166 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "if [ -f docs/adrs/INDEX.md ]; then printf '=== ARCHITECTURE DECISIONS (docs/adrs/) ===\\n\\nRead a full ADR before changing logic in the area it covers.\\n\\n'; cat docs/adrs/INDEX.md; fi"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PreCompact": [
|
|
15
|
+
{
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "if [ -f docs/adrs/HOOK-PROMPT.md ]; then printf '=== ADR CHECK BEFORE COMPACTION ===\\n\\n'; cat docs/adrs/HOOK-PROMPT.md; fi"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"SessionEnd": [
|
|
25
|
+
{
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "if [ -f docs/adrs/HOOK-PROMPT.md ]; then printf '=== ADR CHECK BEFORE SESSION END ===\\n\\n'; cat docs/adrs/HOOK-PROMPT.md; fi"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: release-openaidr
|
|
3
|
+
description: Use when cutting a new openaidr PyPI release. Walks through version bump, release-notes drafting, pre-flight checks, tag, and push — enforces the publish-pypi.yml notes-file gate so the GitHub Releases page can't silently drift from PyPI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# release-openaidr
|
|
7
|
+
|
|
8
|
+
End-to-end release procedure for the `openaidr` distribution.
|
|
9
|
+
`.github/workflows/publish-pypi.yml` enforces the machine-checkable gates
|
|
10
|
+
(tag on main, tag matches both version strings, notes file present, four
|
|
11
|
+
CI gates green). This skill is the human-side counterpart: it drafts the
|
|
12
|
+
notes with judgment, runs the pre-flight checks, and pushes the tag.
|
|
13
|
+
|
|
14
|
+
**Announce at start:** "I'm using the release-openaidr skill to cut a new
|
|
15
|
+
openaidr release."
|
|
16
|
+
|
|
17
|
+
## When to invoke
|
|
18
|
+
|
|
19
|
+
User says any of: "cut a release", "ship 0.1.0", "release openaidr",
|
|
20
|
+
"tag a new version", or starts editing `pyproject.toml`'s version field
|
|
21
|
+
in this repo.
|
|
22
|
+
|
|
23
|
+
## Inputs
|
|
24
|
+
|
|
25
|
+
1. **Target version** (e.g. `0.1.0`). Inferred from `pyproject.toml` if
|
|
26
|
+
the user already bumped it; otherwise ask.
|
|
27
|
+
2. **Theme** — the H1 line of the notes file. Ask the user; a commit log
|
|
28
|
+
can't infer it.
|
|
29
|
+
|
|
30
|
+
## Procedure
|
|
31
|
+
|
|
32
|
+
### Step 1. Pre-flight checks
|
|
33
|
+
|
|
34
|
+
Run from the repo root. Stop and ask if any fails — do not "fix" a dirty
|
|
35
|
+
tree or out-of-sync main automatically, those signal in-flight work.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git rev-parse --abbrev-ref HEAD # must be main
|
|
39
|
+
git status --porcelain # must be empty
|
|
40
|
+
git fetch --tags origin
|
|
41
|
+
git rev-parse --verify "v<version>" 2>/dev/null # MUST FAIL — tag must not exist
|
|
42
|
+
test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
PyPI versions are immutable, including yanked ones. If `<version>` is
|
|
46
|
+
already on PyPI, the only path forward is a new version number:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl -s https://pypi.org/pypi/openaidr/json | python3 -c 'import json,sys; print(sorted(json.load(sys.stdin)["releases"]))'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
(A 404 here means the project has never been published — expected before
|
|
53
|
+
the first release. See "First release" below for what that additionally
|
|
54
|
+
requires.)
|
|
55
|
+
|
|
56
|
+
### Step 2. Gather the commit log
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
prev_tag=$(git describe --tags --abbrev=0) # empty on the first release
|
|
60
|
+
git log --oneline "${prev_tag}..HEAD"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Raw material only — do not dump it into the notes file verbatim.
|
|
64
|
+
|
|
65
|
+
### Step 2b. Span-identity and model-shape drift check (don't skip)
|
|
66
|
+
|
|
67
|
+
OpenAIDR's consumer-visible contract is the session model and, above all,
|
|
68
|
+
span identity: it names one tool call and must survive re-reads of a
|
|
69
|
+
growing session, because a consumer draws a call when parsed and attaches
|
|
70
|
+
outcome and enrichments later by naming the same span. A change to how it
|
|
71
|
+
is derived breaks every consumer, and it **fails silently** — enrichments
|
|
72
|
+
land on the wrong row. Nothing in CI can catch that for a downstream repo.
|
|
73
|
+
|
|
74
|
+
Diff the contract-defining files since the previous release:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# session/turn/tool-call model + span identity derivation
|
|
78
|
+
git diff "${prev_tag}..HEAD" -- src/openaidr/model.py
|
|
79
|
+
# per-kind normalisation rules: agent kind, (server, tool) split, status mapping
|
|
80
|
+
git diff "${prev_tag}..HEAD" -- src/openaidr/kinds.py src/openaidr/toolnames.py src/openaidr/readers/
|
|
81
|
+
# the spec that documents the contract
|
|
82
|
+
git diff "${prev_tag}..HEAD" -- docs/specs/session-collection.md
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Decision gate — stop and ask the user** if either holds:
|
|
86
|
+
|
|
87
|
+
- Span-identity derivation changed (different inputs, different ordering,
|
|
88
|
+
a new component) → it goes under `## Compatibility` in its own
|
|
89
|
+
paragraph, named as breaking, with what a consumer must do about stored
|
|
90
|
+
spans. Never bury it in a highlights bullet.
|
|
91
|
+
- The model's field set or normalisation output changed (a new status, a
|
|
92
|
+
renamed field, a different `(server, tool)` split for an existing kind)
|
|
93
|
+
but `docs/specs/session-collection.md` did not → update the spec, or
|
|
94
|
+
say why it still holds.
|
|
95
|
+
|
|
96
|
+
Adding coverage for a *new* agent kind is not a contract change — that's
|
|
97
|
+
a highlights bullet, not a compatibility note. The gate is about existing
|
|
98
|
+
kinds' output changing shape.
|
|
99
|
+
|
|
100
|
+
If nothing drifted, say so in one line in the release PR so the next
|
|
101
|
+
release knows the check ran and wasn't an oversight.
|
|
102
|
+
|
|
103
|
+
### Step 2c. Dependency and licence sweep
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git diff "${prev_tag}..HEAD" -- pyproject.toml uv.lock | head -60
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `adr-sensor` is pinned exactly (`==`). A bump is consumer-visible —
|
|
110
|
+
it changes which agent formats parse and how — so it earns its own
|
|
111
|
+
bullet naming the old and new version.
|
|
112
|
+
- New runtime dependencies must be Apache-2.0-compatible, and must not
|
|
113
|
+
drag in anything proprietary. OpenAIDR's standing rule: it installs and
|
|
114
|
+
runs correctly for someone who has never heard of any downstream
|
|
115
|
+
product. Check the notes file for that too — no closed-product names.
|
|
116
|
+
|
|
117
|
+
### Step 3. Draft `docs/releases/v<version>.md`
|
|
118
|
+
|
|
119
|
+
Follow the shape documented in `docs/releases/README.md`:
|
|
120
|
+
|
|
121
|
+
```markdown
|
|
122
|
+
# <version> — <theme>
|
|
123
|
+
|
|
124
|
+
## Highlights
|
|
125
|
+
|
|
126
|
+
- **<Theme A>.** 1-3 sentences pitched at someone writing a consumer
|
|
127
|
+
against the session model. Reference ADRs by number when a design doc
|
|
128
|
+
backs the change.
|
|
129
|
+
- **Bug fixes.** Combine small fixes into one bullet, comma-separated.
|
|
130
|
+
|
|
131
|
+
## Install
|
|
132
|
+
|
|
133
|
+
`uv tool install openaidr==<version>` or `pip install openaidr==<version>`.
|
|
134
|
+
|
|
135
|
+
## Compatibility
|
|
136
|
+
|
|
137
|
+
<Behavior changes existing consumers will notice — span identity first if
|
|
138
|
+
it moved. Say "pre-alpha, no back-compat hedging" while that's still true.>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Judgment guidelines (apply ruthlessly):**
|
|
142
|
+
|
|
143
|
+
- **Group by theme, not by commit.** Five commits adding one agent kind's
|
|
144
|
+
reader are one bullet.
|
|
145
|
+
- **Lead with user-visible impact**, not implementation. "Codex sessions
|
|
146
|
+
now report MCP server names" — not "refactored `_split_tool_name`."
|
|
147
|
+
- **Cut chore/format/docs-only and release-machinery commits.** They
|
|
148
|
+
aren't release-visible.
|
|
149
|
+
- **Acknowledge breaking changes explicitly** under `## Compatibility` —
|
|
150
|
+
a changed span identity, a renamed model field, or a different status
|
|
151
|
+
mapping goes there.
|
|
152
|
+
|
|
153
|
+
Show the draft to the user and iterate until they agree it captures what
|
|
154
|
+
shipped.
|
|
155
|
+
|
|
156
|
+
### Step 4. Bump both version strings together
|
|
157
|
+
|
|
158
|
+
`pyproject.toml`'s `version` and `src/openaidr/__init__.py`'s
|
|
159
|
+
`__version__` are separate sources; the workflow fails the build if they
|
|
160
|
+
disagree with the tag. Move them together:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
sed -i.bak 's/^version = ".*"/version = "<version>"/' pyproject.toml && rm pyproject.toml.bak
|
|
164
|
+
sed -i.bak 's/^__version__ = ".*"/__version__ = "<version>"/' src/openaidr/__init__.py && rm src/openaidr/__init__.py.bak
|
|
165
|
+
uv lock # refresh uv.lock
|
|
166
|
+
uv run openaidr --version # sanity: prints openaidr <version>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Step 5. Release-prep commit + PR
|
|
170
|
+
|
|
171
|
+
If the user's original request did not explicitly ask to cut, ship, or
|
|
172
|
+
publish a release, stop before the push and ask. Editing the version
|
|
173
|
+
field is enough to invoke this skill; it is not permission to publish.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
git checkout -b release/<version>
|
|
177
|
+
git add pyproject.toml uv.lock src/openaidr/__init__.py docs/releases/v<version>.md
|
|
178
|
+
git status --porcelain # inspect anything still unstaged; stage intentionally or stop
|
|
179
|
+
git commit -m "release: <version> — <theme>"
|
|
180
|
+
git push -u origin release/<version>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Opening the PR needs its own explicit ask. Then:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
gh pr create --title "release: <version>" --body "Release prep for <version>.
|
|
187
|
+
|
|
188
|
+
- Bumps \`pyproject.toml\` and \`__version__\` to <version>
|
|
189
|
+
- Adds release notes at \`docs/releases/v<version>.md\`
|
|
190
|
+
- After merge: tag \`v<version>\` on main and push to trigger PyPI publish + GitHub Release"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Surface the PR URL. **Stop here.** The user reviews and merges.
|
|
194
|
+
|
|
195
|
+
### Step 6. After merge: tag + push
|
|
196
|
+
|
|
197
|
+
After the user confirms the PR merged, on a clean main:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
git checkout main
|
|
201
|
+
git pull --ff-only
|
|
202
|
+
git tag v<version>
|
|
203
|
+
git push origin v<version>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The tag-triggered run registers asynchronously, so `--limit 1` can grab a
|
|
207
|
+
stale earlier run. Poll for the run tied to the tagged commit:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
tag_sha=$(git rev-parse "v<version>^{commit}")
|
|
211
|
+
run_id=""
|
|
212
|
+
until [ -n "$run_id" ]; do
|
|
213
|
+
run_id=$(gh run list --repo open-agent-security/openaidr \
|
|
214
|
+
--workflow publish-pypi.yml --commit "$tag_sha" \
|
|
215
|
+
--json databaseId --jq '.[0].databaseId // empty')
|
|
216
|
+
[ -z "$run_id" ] && sleep 5
|
|
217
|
+
done
|
|
218
|
+
gh run watch --repo open-agent-security/openaidr "$run_id"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Step 7. Verify
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
curl -s https://pypi.org/pypi/openaidr/json | python3 -c 'import json,sys; print(sorted(json.load(sys.stdin)["releases"]))'
|
|
225
|
+
gh release view v<version> --repo open-agent-security/openaidr --json name,body
|
|
226
|
+
uvx --from openaidr==<version> openaidr --version # installs from PyPI, must print <version>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
All three must succeed. If any fails, surface the workflow run URL rather
|
|
230
|
+
than guessing.
|
|
231
|
+
|
|
232
|
+
## First release
|
|
233
|
+
|
|
234
|
+
Before the *first* tag only, a human must have done two things that no
|
|
235
|
+
workflow can do for itself. Check both and stop if either is missing:
|
|
236
|
+
|
|
237
|
+
1. A **pending** Trusted Publisher on
|
|
238
|
+
<https://pypi.org/manage/account/publishing/> — project `openaidr`,
|
|
239
|
+
owner `open-agent-security`, repository `openaidr`, workflow
|
|
240
|
+
`publish-pypi.yml`, environment `pypi`. Pending, because the project
|
|
241
|
+
does not exist on PyPI until the first successful upload.
|
|
242
|
+
2. A repo **environment named `pypi`** (Settings → Environments). The
|
|
243
|
+
name must match the publisher config exactly or the OIDC exchange is
|
|
244
|
+
rejected — which surfaces as a confusing `invalid-publisher` error at
|
|
245
|
+
the very last step, after a green build.
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
gh api repos/open-agent-security/openaidr/environments --jq '.environments[].name'
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Nothing else about the first release differs: same notes file, same gates.
|
|
252
|
+
|
|
253
|
+
## Failure modes this prevents
|
|
254
|
+
|
|
255
|
+
1. **On PyPI but not on GitHub Releases.** The workflow's notes-file gate
|
|
256
|
+
runs before publish: no notes file at the tag → no PyPI upload.
|
|
257
|
+
2. **Notes file is a raw commit dump.** Step 3's judgment guidelines.
|
|
258
|
+
3. **Tagging a non-main commit.** The workflow's "Verify tag is on main".
|
|
259
|
+
4. **Tag ≠ `pyproject.toml` version, or `__version__` drift.** Two
|
|
260
|
+
separate workflow steps; Step 4 keeps them moving together.
|
|
261
|
+
5. **Forgetting to refresh `uv.lock`.** `uv sync --frozen` fails in the
|
|
262
|
+
build job; Step 4 bumps the lock to avoid the round-trip.
|
|
263
|
+
6. **Silent span-identity or model-shape drift.** Step 2b — the one
|
|
264
|
+
breakage no CI gate in this repo can see, because it only shows up in
|
|
265
|
+
a consumer that stored spans from an earlier version.
|
|
266
|
+
|
|
267
|
+
## What this skill does NOT do
|
|
268
|
+
|
|
269
|
+
- **Doesn't publish to PyPI directly.** The workflow does, via Trusted
|
|
270
|
+
Publishing (OIDC). There is no PyPI token anywhere in this repo, and
|
|
271
|
+
none should be added.
|
|
272
|
+
- **Doesn't create the GitHub Release directly.** The workflow's
|
|
273
|
+
`release-github` job does, from the notes file.
|
|
274
|
+
- **Doesn't write a CHANGELOG.md.** The convention is per-release files
|
|
275
|
+
under `docs/releases/`.
|
|
276
|
+
- **Doesn't yank.** PyPI versions are immutable; a broken release is
|
|
277
|
+
fixed by shipping the next version, not by re-uploading.
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
name: Autofix
|
|
2
|
+
|
|
3
|
+
# Two-direction comment-bounce that closes the Codex/Claude review loop
|
|
4
|
+
# without human intervention:
|
|
5
|
+
#
|
|
6
|
+
# 1. Codex bot reviews a PR -> this workflow posts `@claude please
|
|
7
|
+
# address...` -> claude.yml fires, claude reads the review, fixes
|
|
8
|
+
# it, pushes commits, posts a "Claude finished" summary comment.
|
|
9
|
+
#
|
|
10
|
+
# 2. Any new commits land on a PR head (`pull_request: synchronize`) ->
|
|
11
|
+
# this workflow posts `@codex review` -> Codex re-reviews HEAD. ONE
|
|
12
|
+
# job covers every source of new commits — the claude bot's fixes, a
|
|
13
|
+
# human / local-agent push, AND the auto-rebase force-push below —
|
|
14
|
+
# because `synchronize` fires regardless of who pushed. (Codex
|
|
15
|
+
# auto-reviews on PR open / draft->ready natively, but NOT on
|
|
16
|
+
# subsequent pushes; without this, later commits land unreviewed.)
|
|
17
|
+
#
|
|
18
|
+
# The two bounces together produce: codex-flag -> claude-fix ->
|
|
19
|
+
# codex-rereview -> ... bounded by the claude-side iteration cap in
|
|
20
|
+
# bounce 1 (the cap counts claude runs, so push-driven re-reviews add
|
|
21
|
+
# entry points, not an unbounded cycle). Codex is silent when clean
|
|
22
|
+
# (thumbs reaction), so an unnecessary @codex review on a no-op SHA costs
|
|
23
|
+
# one cheap call with no PR noise.
|
|
24
|
+
#
|
|
25
|
+
# Authentication: comments are posted (and PR branches are rebased)
|
|
26
|
+
# using AUTOFIX_TRIGGER_PAT, NOT secrets.GITHUB_TOKEN. GitHub explicitly
|
|
27
|
+
# suppresses workflow runs triggered by events created with
|
|
28
|
+
# GITHUB_TOKEN (recursion guard) — so a comment posted under
|
|
29
|
+
# github-actions[bot] would be silently dropped before claude.yml ever
|
|
30
|
+
# fired, and a rebase pushed under GITHUB_TOKEN would not re-trigger
|
|
31
|
+
# CI / Codex on the rebased branch. The PAT is fine-grained, owned by
|
|
32
|
+
# a repo OWNER/MEMBER, with BOTH:
|
|
33
|
+
# - `Pull requests: Read and write` — for `gh pr comment` (bounces 1
|
|
34
|
+
# and 2) and `gh pr list` (bounce 0).
|
|
35
|
+
# - `Contents: Read and write` — for `actions/checkout` to fetch and
|
|
36
|
+
# `git push` from the rebase-stale-prs job. WITHOUT this, the job
|
|
37
|
+
# fails at the `Checkout` step with `403: Write access to
|
|
38
|
+
# repository not granted` — actions/checkout asks GitHub to verify
|
|
39
|
+
# write permission upfront because the same token will push later.
|
|
40
|
+
# The resulting comments/pushes are authored by that user, which DOES
|
|
41
|
+
# trigger downstream workflows AND naturally passes claude.yml's
|
|
42
|
+
# OWNER/MEMBER/COLLABORATOR trust gate. The same PAT serves all three
|
|
43
|
+
# bounces — Codex's webhook listener watches @-mentions in PR comments
|
|
44
|
+
# regardless of author.
|
|
45
|
+
#
|
|
46
|
+
# If AUTOFIX_TRIGGER_PAT is unset, both `gh pr comment` paths fail
|
|
47
|
+
# with an auth error and the chain breaks; rebase-stale-prs detects
|
|
48
|
+
# the missing secret and skips with a workflow warning. Manual
|
|
49
|
+
# @claude triggers are unaffected.
|
|
50
|
+
#
|
|
51
|
+
# Workflow-injection safety: every user/bot-controlled input (PR number,
|
|
52
|
+
# comment body, review id) is read via env: and referenced as a shell
|
|
53
|
+
# variable. Comment bodies posted to the PR are hardcoded literals — no
|
|
54
|
+
# interpolation of untrusted input into shell.
|
|
55
|
+
|
|
56
|
+
on:
|
|
57
|
+
pull_request_review:
|
|
58
|
+
types: [submitted]
|
|
59
|
+
pull_request:
|
|
60
|
+
# Bounce 2: new commits on a PR head -> request a Codex re-review.
|
|
61
|
+
# `synchronize` fires for pushes from ANY author — claude bot fixes,
|
|
62
|
+
# human / local-agent pushes, AND the auto-rebase force-push from
|
|
63
|
+
# rebase-stale-prs. App-authored pushes (claude[bot]) are NOT the
|
|
64
|
+
# workflow's GITHUB_TOKEN, so they are not recursion-suppressed and DO
|
|
65
|
+
# fire this event (verified: CI runs on claude's pushed commits).
|
|
66
|
+
types: [synchronize]
|
|
67
|
+
push:
|
|
68
|
+
# Bounce 0: main moved -> rebase open PRs that are now stale. Catches
|
|
69
|
+
# both PR merges AND direct pushes to main (manual hotfixes, manual
|
|
70
|
+
# main pushes). pull_request:closed+merged would miss the latter.
|
|
71
|
+
branches: [main]
|
|
72
|
+
|
|
73
|
+
jobs:
|
|
74
|
+
trigger-claude-via-comment:
|
|
75
|
+
# Bounce 1: Codex review submitted -> @claude addresses it.
|
|
76
|
+
if: github.event_name == 'pull_request_review' && github.event.review.user.login == 'chatgpt-codex-connector[bot]' && github.event.review.state == 'commented'
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
permissions:
|
|
79
|
+
pull-requests: write
|
|
80
|
+
env:
|
|
81
|
+
GH_TOKEN: ${{ secrets.AUTOFIX_TRIGGER_PAT }}
|
|
82
|
+
REPO: ${{ github.repository }}
|
|
83
|
+
PR_NUM: ${{ github.event.pull_request.number }}
|
|
84
|
+
REVIEW_ID: ${{ github.event.review.id }}
|
|
85
|
+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
86
|
+
steps:
|
|
87
|
+
# Cap the auto-address loop at 7 claude runs per PR. If claude has
|
|
88
|
+
# already run 7+ times and Codex is still finding issues, the loop
|
|
89
|
+
# isn't converging — force human attention rather than burn cycles.
|
|
90
|
+
#
|
|
91
|
+
# Default was 3 originally; bumped to 7 because legitimate Codex
|
|
92
|
+
# reviews on substantive PRs routinely surface 5-6 rounds of real
|
|
93
|
+
# findings (defensive type guards across sibling files, CLI-flag
|
|
94
|
+
# table extensions, OSV event-window edge cases). 3 was capping
|
|
95
|
+
# real review work, not just runaway loops; 7 keeps the safety
|
|
96
|
+
# net while accommodating multi-round legitimate review. Adjust
|
|
97
|
+
# MAX below if your project's review patterns differ.
|
|
98
|
+
#
|
|
99
|
+
# Count claude-code-action runs by counting top-level PR comments
|
|
100
|
+
# authored by the bot (the action posts ONE in-progress comment
|
|
101
|
+
# per run and edits it in place to the "Claude finished..." summary
|
|
102
|
+
# — 1 comment == 1 run). More accurate than counting bot commits:
|
|
103
|
+
# a run that lands 0 commits (no-op fix or pre-push failure) and a
|
|
104
|
+
# run that lands N commits both equal one iteration of the loop.
|
|
105
|
+
#
|
|
106
|
+
# Login string is "claude" — NOT "claude[bot]" — because
|
|
107
|
+
# `gh pr view --json comments` uses GraphQL, and GraphQL's
|
|
108
|
+
# `author.login` returns the bare bot name without the `[bot]`
|
|
109
|
+
# suffix. The `[bot]` suffix only appears in REST surfaces (e.g.
|
|
110
|
+
# `github.event.comment.user.login` at bounce-2 below). Same bot,
|
|
111
|
+
# two string representations depending on which API you read.
|
|
112
|
+
- name: Cap auto-address iterations (max 7)
|
|
113
|
+
run: |
|
|
114
|
+
MAX=7
|
|
115
|
+
CLAUDE_RUNS=$(gh pr view "$PR_NUM" -R "$REPO" --json comments \
|
|
116
|
+
--jq '[.comments[] | select(.author.login == "claude")] | length')
|
|
117
|
+
if [ "$CLAUDE_RUNS" -ge "$MAX" ]; then
|
|
118
|
+
gh pr comment "$PR_NUM" -R "$REPO" --body "🛑 Auto-address loop limit hit — claude has already run $CLAUDE_RUNS times on this PR (max $MAX). Codex review id $REVIEW_ID was NOT auto-addressed. Manual review needed; re-tag claude explicitly after addressing the underlying issue if you want to continue. ([workflow run]($RUN_URL))"
|
|
119
|
+
echo "::error::Auto-address loop limit hit ($CLAUDE_RUNS/$MAX) — see PR comment."
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
echo "Auto-address iteration $((CLAUDE_RUNS + 1)) of $MAX"
|
|
123
|
+
|
|
124
|
+
# Post the @claude trigger. The body intentionally references "the
|
|
125
|
+
# Codex review above" — claude-code-action's default prompt
|
|
126
|
+
# construction will include the full review thread so the bot has
|
|
127
|
+
# the actual findings + line context to act on.
|
|
128
|
+
#
|
|
129
|
+
# No HEAD-SHA marker is needed here: bounce 2 is now driven by
|
|
130
|
+
# `pull_request: synchronize` (a real push), so "claude finished
|
|
131
|
+
# without pushing" simply produces no synchronize and no re-review —
|
|
132
|
+
# the old comment-driven no-push detection it required is gone.
|
|
133
|
+
- name: Post @claude trigger comment
|
|
134
|
+
run: |
|
|
135
|
+
gh pr comment "$PR_NUM" -R "$REPO" --body "@claude please address the Codex review feedback above. CRITICAL: before applying any fix that depends on infrastructure or invariants you have not verified, check the claim against its actual source of truth: for in-repo invariants (DLQ, schema, contracts, ADRs in docs/adrs/), grep the repo for the prerequisite; for claims about EXTERNAL behavior (a third-party SDK's import paths or API contract, a vendor's documented semantics, a wire format), use WebFetch/WebSearch to check the current official docs — do not accept the reviewer's citation on authority, and do not defend repo-internal assertions you cannot falsify. Only if the claim cannot be verified either way, push back via a PR comment explaining exactly what needs human confirmation rather than implementing on a false premise."
|
|
136
|
+
|
|
137
|
+
request-codex-rereview-on-push:
|
|
138
|
+
# Bounce 2: new commits pushed to a PR head -> @codex re-reviews HEAD.
|
|
139
|
+
# `pull_request: synchronize` fires on EVERY push to the PR head
|
|
140
|
+
# regardless of author — the claude bot's fixes, a human / local-agent
|
|
141
|
+
# push, AND the auto-rebase force-push from rebase-stale-prs — so this
|
|
142
|
+
# single job covers all of them. Replaces the old issue_comment /
|
|
143
|
+
# "Claude finished" trigger: claude-code-action commits via the Claude
|
|
144
|
+
# GitHub App (author claude[bot]), and App-authored pushes fire
|
|
145
|
+
# pull_request events (they are not the workflow's GITHUB_TOKEN, so not
|
|
146
|
+
# recursion-suppressed). "Claude finished without pushing" needs no
|
|
147
|
+
# special case now — no commits means no synchronize means no review.
|
|
148
|
+
#
|
|
149
|
+
# Same-repo only: GitHub withholds secrets from pull_request runs on
|
|
150
|
+
# fork PRs, so AUTOFIX_TRIGGER_PAT would be empty and `gh pr comment`
|
|
151
|
+
# would fail. Excluding forks here keeps fork PRs from producing red
|
|
152
|
+
# runs (and claude.yml refuses fork PRs downstream anyway).
|
|
153
|
+
if: >
|
|
154
|
+
github.event_name == 'pull_request'
|
|
155
|
+
&& github.event.pull_request.head.repo.full_name == github.repository
|
|
156
|
+
runs-on: ubuntu-latest
|
|
157
|
+
permissions:
|
|
158
|
+
pull-requests: write
|
|
159
|
+
concurrency:
|
|
160
|
+
# Coalesce rapid pushes to one review of the final SHA. Without this,
|
|
161
|
+
# 5 quick commits = 5 separate @codex reviews (each distinct SHA
|
|
162
|
+
# dodges the per-SHA marker). cancel-in-progress keeps only the
|
|
163
|
+
# latest push's job, which then reads the current HEAD.
|
|
164
|
+
group: codex-rereview-${{ github.event.pull_request.number }}
|
|
165
|
+
cancel-in-progress: true
|
|
166
|
+
env:
|
|
167
|
+
GH_TOKEN: ${{ secrets.AUTOFIX_TRIGGER_PAT }}
|
|
168
|
+
REPO: ${{ github.repository }}
|
|
169
|
+
PR_NUM: ${{ github.event.pull_request.number }}
|
|
170
|
+
steps:
|
|
171
|
+
# Per-SHA idempotency: skip if we already requested review for the
|
|
172
|
+
# current HEAD. Guards a double-synchronize on the same SHA and any
|
|
173
|
+
# overlap with a manual @codex review. Read live HEAD (not the event
|
|
174
|
+
# SHA) so a coalesced burst reviews the latest commit.
|
|
175
|
+
- name: Post @codex re-review trigger (idempotent per SHA)
|
|
176
|
+
run: |
|
|
177
|
+
HEAD_SHA=$(gh pr view "$PR_NUM" -R "$REPO" --json headRefOid --jq '.headRefOid')
|
|
178
|
+
MARKER="<!-- autofix-rereview-sha:$HEAD_SHA -->"
|
|
179
|
+
ALREADY=$(gh pr view "$PR_NUM" -R "$REPO" --json comments \
|
|
180
|
+
--jq "[.comments[] | select(.body | contains(\"$MARKER\"))] | length")
|
|
181
|
+
if [ "$ALREADY" -gt 0 ]; then
|
|
182
|
+
echo "Already requested @codex review for $HEAD_SHA — skipping."
|
|
183
|
+
exit 0
|
|
184
|
+
fi
|
|
185
|
+
gh pr comment "$PR_NUM" -R "$REPO" --body "$MARKER
|
|
186
|
+
@codex review the latest commits pushed to this PR."
|
|
187
|
+
|
|
188
|
+
rebase-stale-prs:
|
|
189
|
+
# Bounce 0: main moved -> for each open PR (same-repo only, fork PRs
|
|
190
|
+
# excluded), check mergeStateStatus and either rebase cleanly or ask
|
|
191
|
+
# @claude to resolve conflicts. Catches both PR-merge pushes and
|
|
192
|
+
# direct pushes to main.
|
|
193
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
194
|
+
runs-on: ubuntu-latest
|
|
195
|
+
permissions:
|
|
196
|
+
contents: write
|
|
197
|
+
pull-requests: write
|
|
198
|
+
concurrency:
|
|
199
|
+
# Coalesce: if main moves twice while we're still working on the
|
|
200
|
+
# first push, cancel and re-run with the latest SHA. Avoids racing
|
|
201
|
+
# two rebase runs against the same PR.
|
|
202
|
+
group: rebase-stale-prs
|
|
203
|
+
cancel-in-progress: true
|
|
204
|
+
env:
|
|
205
|
+
# Use the PAT for both gh API calls AND the git push. Pushing to a
|
|
206
|
+
# PR branch under GITHUB_TOKEN works but invalidates downstream
|
|
207
|
+
# workflow triggers (recursion guard) — the PAT keeps the @claude
|
|
208
|
+
# comment path working if this rebase later races with a Codex
|
|
209
|
+
# review on the same PR.
|
|
210
|
+
GH_TOKEN: ${{ secrets.AUTOFIX_TRIGGER_PAT }}
|
|
211
|
+
REPO: ${{ github.repository }}
|
|
212
|
+
MAIN_SHA: ${{ github.sha }}
|
|
213
|
+
steps:
|
|
214
|
+
# Graceful skip when the PAT secret isn't configured yet (initial
|
|
215
|
+
# repo setup). Without this guard, actions/checkout fails with
|
|
216
|
+
# "Input required and not supplied: token" on every push to main,
|
|
217
|
+
# turning every merge into a red workflow run during the window
|
|
218
|
+
# between "workflows scaffolded" and "secrets configured."
|
|
219
|
+
- id: have_pat
|
|
220
|
+
name: Check AUTOFIX_TRIGGER_PAT presence
|
|
221
|
+
env:
|
|
222
|
+
PAT: ${{ secrets.AUTOFIX_TRIGGER_PAT }}
|
|
223
|
+
run: |
|
|
224
|
+
if [ -z "$PAT" ]; then
|
|
225
|
+
echo "::warning::AUTOFIX_TRIGGER_PAT not set; skipping rebase. Add the secret per setup-autofix instructions to enable."
|
|
226
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
227
|
+
else
|
|
228
|
+
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
- name: Checkout (full history for rebase)
|
|
232
|
+
if: steps.have_pat.outputs.skip != 'true'
|
|
233
|
+
uses: actions/checkout@v4
|
|
234
|
+
with:
|
|
235
|
+
fetch-depth: 0
|
|
236
|
+
token: ${{ secrets.AUTOFIX_TRIGGER_PAT }}
|
|
237
|
+
|
|
238
|
+
- name: Configure git author
|
|
239
|
+
if: steps.have_pat.outputs.skip != 'true'
|
|
240
|
+
run: |
|
|
241
|
+
git config user.email "actions@users.noreply.github.com"
|
|
242
|
+
git config user.name "github-actions[bot]"
|
|
243
|
+
|
|
244
|
+
- name: Rebase BEHIND PRs / flag DIRTY ones
|
|
245
|
+
if: steps.have_pat.outputs.skip != 'true'
|
|
246
|
+
run: |
|
|
247
|
+
set -euo pipefail
|
|
248
|
+
|
|
249
|
+
# mergeStateStatus values that matter:
|
|
250
|
+
# CLEAN - up to date with main (or ahead). skip.
|
|
251
|
+
# BEHIND - mergeable, base moved. clean rebase needed.
|
|
252
|
+
# DIRTY - conflicts. ask @claude.
|
|
253
|
+
# BLOCKED - branch protection or required checks failing. skip.
|
|
254
|
+
# UNSTABLE - mergeable but failing checks. skip — pre-existing.
|
|
255
|
+
# UNKNOWN - GitHub still computing. skip; next push retries.
|
|
256
|
+
#
|
|
257
|
+
# We exclude fork PRs: pushing to a fork branch from this workflow
|
|
258
|
+
# 403s, and posting @claude on a fork is blocked downstream by
|
|
259
|
+
# claude.yml's fork-refusal step anyway.
|
|
260
|
+
PRS_JSON=$(gh pr list --state open -R "$REPO" \
|
|
261
|
+
--json number,headRefName,mergeStateStatus,baseRefName,headRepository \
|
|
262
|
+
--jq "[.[] | select(.baseRefName == \"main\") | select(.headRepository.nameWithOwner == \"$REPO\")]")
|
|
263
|
+
|
|
264
|
+
echo "Open same-repo PRs targeting main:"
|
|
265
|
+
echo "$PRS_JSON" | jq -r '.[] | " #\(.number) [\(.mergeStateStatus)] \(.headRefName)"'
|
|
266
|
+
|
|
267
|
+
# Process each PR. Loop over JSON array via index so we can read
|
|
268
|
+
# only env-style fields and never interpolate untrusted data.
|
|
269
|
+
COUNT=$(echo "$PRS_JSON" | jq 'length')
|
|
270
|
+
for i in $(seq 0 $((COUNT - 1))); do
|
|
271
|
+
PR_NUM=$(echo "$PRS_JSON" | jq -r ".[$i].number")
|
|
272
|
+
BRANCH=$(echo "$PRS_JSON" | jq -r ".[$i].headRefName")
|
|
273
|
+
STATUS=$(echo "$PRS_JSON" | jq -r ".[$i].mergeStateStatus")
|
|
274
|
+
|
|
275
|
+
# Defensive: branch names are user-controlled. Refuse anything
|
|
276
|
+
# that isn't a sane ref. (`refs/heads/<name>` syntax: letters,
|
|
277
|
+
# digits, slashes, dashes, underscores, dots.)
|
|
278
|
+
if ! printf '%s' "$BRANCH" | grep -qE '^[A-Za-z0-9._/-]+$'; then
|
|
279
|
+
echo "::warning::PR #$PR_NUM has unsafe branch name; skipping"
|
|
280
|
+
continue
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
case "$STATUS" in
|
|
284
|
+
BEHIND)
|
|
285
|
+
echo "PR #$PR_NUM ($BRANCH): clean rebase needed"
|
|
286
|
+
git fetch origin "$BRANCH":"refs/remotes/origin/$BRANCH" --force
|
|
287
|
+
git checkout -B "$BRANCH" "origin/$BRANCH"
|
|
288
|
+
if git rebase origin/main; then
|
|
289
|
+
if git push --force-with-lease origin "$BRANCH"; then
|
|
290
|
+
gh pr comment "$PR_NUM" -R "$REPO" \
|
|
291
|
+
--body "🔁 Auto-rebased on \`main\` (no conflicts) after $MAIN_SHA."
|
|
292
|
+
else
|
|
293
|
+
echo "::warning::Force-push of #$PR_NUM failed (lease lost?). Will retry on next push."
|
|
294
|
+
fi
|
|
295
|
+
else
|
|
296
|
+
# Race: GitHub said BEHIND but the rebase produced
|
|
297
|
+
# conflicts. The next push will see DIRTY and route to
|
|
298
|
+
# the @claude path.
|
|
299
|
+
git rebase --abort
|
|
300
|
+
echo "::warning::Rebase of #$PR_NUM failed despite BEHIND status (likely race with another push)"
|
|
301
|
+
fi
|
|
302
|
+
git checkout main
|
|
303
|
+
;;
|
|
304
|
+
DIRTY)
|
|
305
|
+
echo "PR #$PR_NUM ($BRANCH): conflicts — asking @claude to rebase"
|
|
306
|
+
# Idempotency: don't re-ask claude for the same main SHA.
|
|
307
|
+
MARKER="<!-- autofix-rebase-claude:$MAIN_SHA -->"
|
|
308
|
+
ALREADY=$(gh pr view "$PR_NUM" -R "$REPO" --json comments \
|
|
309
|
+
--jq "[.comments[] | select(.body | contains(\"$MARKER\"))] | length")
|
|
310
|
+
if [ "$ALREADY" -gt 0 ]; then
|
|
311
|
+
echo " already asked @claude for $MAIN_SHA — skipping"
|
|
312
|
+
continue
|
|
313
|
+
fi
|
|
314
|
+
gh pr comment "$PR_NUM" -R "$REPO" --body "$MARKER
|
|
315
|
+
@claude main has moved and this PR has merge conflicts. Rebase \`$BRANCH\` onto the LATEST \`origin/main\` and resolve the conflicts.
|
|
316
|
+
|
|
317
|
+
CRITICAL — main may move again while you work (resolve + gate take minutes), so a rebase onto the main you first fetched can be stale by the time you push. Immediately before force-pushing, run \`git fetch origin main\` and verify ALL of:
|
|
318
|
+
1. \`git merge-base --is-ancestor origin/main HEAD\` exits 0 — the branch is on top of CURRENT main. If it fails, main advanced mid-rebase: re-fetch and rebase again.
|
|
319
|
+
2. \`git log --oneline origin/main..HEAD\` shows ONLY this PR's own commits — no main commits and no merge commit (a merge commit means you merged instead of rebased).
|
|
320
|
+
3. the local pre-push gate passes.
|
|
321
|
+
Then force-push. AFTER pushing, confirm \`gh pr view $PR_NUM --json mergeStateStatus -q .mergeStateStatus\` is no longer DIRTY/CONFLICTING (wait for it to leave UNKNOWN — GitHub recomputes for a few seconds). Do NOT report the rebase as done until that check passes; if it stays conflicting or main keeps moving, say so in a comment instead of force-fitting a resolution.
|
|
322
|
+
|
|
323
|
+
If the conflicts indicate the new main invalidates this PR's approach, push back via a comment instead of resolving."
|
|
324
|
+
;;
|
|
325
|
+
CLEAN)
|
|
326
|
+
echo "PR #$PR_NUM ($BRANCH): already current — skipping"
|
|
327
|
+
;;
|
|
328
|
+
*)
|
|
329
|
+
echo "PR #$PR_NUM ($BRANCH): status $STATUS — leaving alone"
|
|
330
|
+
;;
|
|
331
|
+
esac
|
|
332
|
+
done
|