Repository navigation
341 lines (324 loc) · 14.7 KB
/
Copy pathpython.yml
File metadata and controls
341 lines (324 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
name: Python
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
# A newer push to a pull request replaces its unfinished run; main runs each commit to the end.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
release-metadata:
name: Validate release metadata
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.version.outputs.changed }}
tag: ${{ steps.version.outputs.tag }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"
- name: Verify paired source archive
run: |
python - <<'PY'
import io
import subprocess
import tarfile
skill = "skills/agentnave-manager"
required = ["pyproject.toml", "src/agentnave/__init__.py", f"{skill}/SKILL.md",
"src/agentnave/mcp_server.py", "src/agentnave/conversations.py",
f"{skill}/references/studio.md"]
required += [f"{skill}/references/{name}.md" for name in
("claude", "codebuddy", "codex", "grok", "antigravity", "pi", "deepseek", "zcode")]
required += ["skills/agentnave-review/SKILL.md",
"skills/agentnave-review/references/reviewers.md",
"skills/agentnave-handover/SKILL.md",
"skills/agentnave-handover/references/sessions.md"]
archive = subprocess.check_output(["git", "archive", "--format=tar", "HEAD", "--", *required])
with tarfile.open(fileobj=io.BytesIO(archive)) as source:
for name in required:
member = source.getmember(name)
assert member.isfile() and member.size > 0, f"Missing source content: {name}"
PY
- name: Check version change and release notes
id: version
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: |
python - <<'PY'
import ast
import os
import re
import subprocess
import tomllib
from pathlib import Path
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
if not re.fullmatch(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)", version):
raise SystemExit("Releases require a stable MAJOR.MINOR.PATCH version")
def runtime_version(path: str) -> list[object]:
module = ast.parse(Path(path).read_text())
return [
ast.literal_eval(node.value)
for node in module.body
if isinstance(node, ast.Assign)
and any(isinstance(target, ast.Name) and target.id == "__version__" for target in node.targets)
]
if runtime_version("src/agentnave/__init__.py") != [version]:
raise SystemExit("pyproject.toml and agentnave.__version__ must match")
previous = tomllib.loads(subprocess.check_output(
["git", "show", f"{os.environ['BASE_SHA']}:pyproject.toml"], text=True
))["project"]["version"]
changed = version != previous
if changed:
if tuple(map(int, version.split("."))) <= tuple(map(int, previous.split("."))):
raise SystemExit("A release version must increase")
notes = Path(f"docs/releases/v{version}.md")
if not notes.is_file() or not notes.read_text().strip():
raise SystemExit(f"Missing release notes: {notes}")
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
output.write(f"changed={str(changed).lower()}\ntag=v{version}\n")
print(f"{previous} -> {version}; release required: {changed}")
PY
verify:
name: Python 3.12 / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
- name: Install dependencies
run: uv sync --locked --all-groups
- name: Check formatting
run: uv run ruff format --check .
- name: Lint
run: uv run ruff check .
- name: Type check
run: uv run pyright
- name: Test
run: uv run pytest
- name: Build distributions
run: uv build
- name: Verify installed distributions
if: runner.os != 'Windows'
env:
UV_TOOL_DIR: ${{ runner.temp }}/agentnave-tools
UV_TOOL_BIN_DIR: ${{ runner.temp }}/agentnave-bin
run: |
uv tool install --python 3.12 dist/agentnave-*.whl
# The retired launcher must be absent; verify_install treats its path as an absence check.
"$UV_TOOL_DIR/agentnave/bin/python" scripts/verify_install.py \
"$UV_TOOL_BIN_DIR/agentnave-mcp" "$UV_TOOL_BIN_DIR/agentnave-studio-mcp"
- name: Verify installed distributions on Windows
if: runner.os == 'Windows'
env:
UV_TOOL_DIR: ${{ runner.temp }}\agentnave-tools
UV_TOOL_BIN_DIR: ${{ runner.temp }}\agentnave-bin
run: |
$wheel = (Get-ChildItem -LiteralPath dist -Filter "agentnave-*.whl" -File -ErrorAction Stop).FullName
uv tool install --python 3.12 $wheel
$python = Join-Path $env:UV_TOOL_DIR "agentnave\Scripts\python.exe"
& $python scripts/verify_install.py (Join-Path $env:UV_TOOL_BIN_DIR "agentnave-mcp.exe") (Join-Path $env:UV_TOOL_BIN_DIR "agentnave-studio-mcp.exe")
if ($LASTEXITCODE -ne 0) { throw "Installed distribution verification failed" }
release:
name: Publish version release
needs: [release-metadata, verify]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.release-metadata.outputs.changed == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
concurrency:
group: release-${{ github.sha }}
cancel-in-progress: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
- name: Create tag and GitHub Release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ needs.release-metadata.outputs.tag }}
run: |
tag_exists=false
if git show-ref --verify --quiet "refs/tags/$RELEASE_TAG"; then
tag_exists=true
test "$(git rev-parse "refs/tags/$RELEASE_TAG^{commit}")" = "$GITHUB_SHA" || {
echo "Refusing to move an existing release tag: $RELEASE_TAG" >&2
exit 1
}
fi
if draft=$(gh release view "$RELEASE_TAG" --json isDraft --jq .isDraft); then
test "$tag_exists" = true || {
echo "An existing release has no tag: $RELEASE_TAG" >&2
exit 1
}
test "$draft" = false || {
echo "An existing draft requires manual review: $RELEASE_TAG" >&2
exit 1
}
echo "Release already published: $RELEASE_TAG"
else
if [ "$tag_exists" = false ]; then
gh api --method POST "repos/$GH_REPO/git/refs" \
-f ref="refs/tags/$RELEASE_TAG" -f sha="$GITHUB_SHA"
fi
gh release create "$RELEASE_TAG" --verify-tag \
--title "AgentNave $RELEASE_TAG" --notes-file "docs/releases/$RELEASE_TAG.md"
fi
uv build --wheel
gh release upload "$RELEASE_TAG" dist/agentnave-*.whl --clobber
tray-release:
name: Attach macOS tray app
needs: [release-metadata, release]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.release-metadata.outputs.changed == 'true'
runs-on: macos-latest
permissions:
contents: write
concurrency:
group: tray-release-${{ github.sha }}
cancel-in-progress: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Build signed universal app
working-directory: apps/tray
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
test -n "$TAURI_SIGNING_PRIVATE_KEY" || {
echo "Missing repository secret TAURI_SIGNING_PRIVATE_KEY" >&2
exit 1
}
rustup target add aarch64-apple-darwin x86_64-apple-darwin
npx --yes @tauri-apps/cli@2.12.3 build --target universal-apple-darwin \
--bundles app,dmg --config '{"bundle":{"createUpdaterArtifacts":true}}'
- name: Upload installer, update archive and manifest
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ needs.release-metadata.outputs.tag }}
run: |
bundle=apps/tray/target/universal-apple-darwin/release/bundle
mkdir -p tray-assets
cp "$bundle/macos/AgentNave.app.tar.gz" tray-assets/AgentNave_universal.app.tar.gz
cp "$bundle"/dmg/*.dmg "tray-assets/AgentNave_${RELEASE_TAG#v}_universal.dmg"
python3 - "$bundle/macos/AgentNave.app.tar.gz.sig" <<'PY'
import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
tag, repo = os.environ["RELEASE_TAG"], os.environ["GH_REPO"]
# The app's update dialog shows what changed in every release since the installed one:
# the last ten release notes, without their title and upgrade instructions.
def key(path):
return tuple(int(part) for part in path.stem.removeprefix("v").split("."))
notes = []
releases = [p for p in Path("docs/releases").glob("v*.md") if re.fullmatch(r"v\d+\.\d+\.\d+", p.stem)]
for path in sorted(releases, key=key, reverse=True)[:10]:
text = re.sub(r"\A# .*\n", "", path.read_text())
text = re.split(r"^## 升级\s*$", text, flags=re.M)[0].strip()
notes.append({"version": path.stem.removeprefix("v"), "notes": text})
entry = {
"signature": open(sys.argv[1]).read().strip(),
"url": f"https://github.com/{repo}/releases/download/{tag}/AgentNave_universal.app.tar.gz",
}
manifest = {
"version": tag.removeprefix("v"),
"notes": json.dumps(notes, ensure_ascii=False),
"pub_date": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"platforms": {"darwin-aarch64": entry, "darwin-x86_64": entry},
}
with open("tray-assets/latest.json", "w") as output:
json.dump(manifest, output, indent=2)
PY
gh release upload "$RELEASE_TAG" tray-assets/* --clobber
windows-release:
name: Attach Windows installer
# After the macOS job: that is what publishes latest.json, and this one adds its platform.
needs: [release-metadata, release, tray-release]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.release-metadata.outputs.changed == 'true'
runs-on: windows-latest
permissions:
contents: write
concurrency:
group: windows-release-${{ github.sha }}
cancel-in-progress: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Build signed installer
working-directory: apps/tray
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
test -n "$TAURI_SIGNING_PRIVATE_KEY" || {
echo "Missing repository secret TAURI_SIGNING_PRIVATE_KEY" >&2
exit 1
}
npx --yes @tauri-apps/cli@2.12.3 build --bundles nsis \
--config '{"bundle":{"createUpdaterArtifacts":true}}'
- name: Upload installer and add its platform to the manifest
shell: bash
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ needs.release-metadata.outputs.tag }}
run: |
version="${RELEASE_TAG#v}"
asset="AgentNave_${version}_x64-setup.exe"
built=$(ls apps/tray/target/release/bundle/nsis/*_x64-setup.exe)
test -f "$built.sig" || {
echo "Missing updater signature next to $built" >&2
exit 1
}
mkdir -p windows-assets
cp "$built" "windows-assets/$asset"
cp "$built.sig" "windows-assets/$asset.sig"
gh release upload "$RELEASE_TAG" windows-assets/* --clobber
# latest.json already lists the macOS platforms; add Windows next to them.
gh release download "$RELEASE_TAG" --pattern latest.json --dir windows-assets
python - <<'PY'
import json
import os
from pathlib import Path
tag, repo = os.environ["RELEASE_TAG"], os.environ["GH_REPO"]
version = tag.removeprefix("v")
asset = f"AgentNave_{version}_x64-setup.exe"
manifest_path = Path("windows-assets/latest.json")
# Explicit UTF-8: Path.read_text() would use the Windows locale codec on this runner.
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest["platforms"]["windows-x86_64"] = {
"signature": Path(f"windows-assets/{asset}.sig").read_text(encoding="utf-8").strip(),
"url": f"https://github.com/{repo}/releases/download/{tag}/{asset}",
}
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
PY
gh release upload "$RELEASE_TAG" windows-assets/latest.json --clobber