-
Notifications
You must be signed in to change notification settings - Fork 1
379 lines (351 loc) · 14.7 KB
/
Copy pathrelease.yml
File metadata and controls
379 lines (351 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
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
name: Release
# Triggered by CI completing on main. We do NOT trigger on push directly
# so a release can never ship if the test matrix failed.
#
# Auto-bump model: every CI-success push to main increments the patch
# version (0.1.0 → 0.1.1 → 0.1.2 → …). The bump itself lands as a
# `chore: bump version to X.Y.Z` commit pushed back to main; that push
# uses GITHUB_TOKEN, which by GitHub's rules does NOT trigger CI again,
# so there's no infinite loop. As belt-and-suspenders we also skip the
# bump if the previous commit was already an auto-bump.
#
# Escape hatch: a commit message containing `[skip release]` opts out
# of bumping AND publishing for that push.
#
# CI (ci.yml) succeeds on main push
# │
# ▼ workflow_run: completed + conclusion == success
# bump-version → publish-pypi → create-draft-release
# │
# ▼
# build-nuitka [linux | windows | macos]
# │
# ▼
# publish-release (unmark draft)
on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]
permissions:
# contents:write is needed to push the bump commit, create tags +
# releases, and upload release assets (the Nuitka binaries).
contents: write
concurrency:
group: release-main
cancel-in-progress: false
jobs:
bump-version:
name: Auto-bump patch version
# Only run when CI succeeded. workflow_run fires on every CI
# completion (success, failure, cancelled), so we have to gate
# this explicitly.
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.bump.outputs.version }}
sha: ${{ steps.push.outputs.sha }}
skipped: ${{ steps.gate.outputs.skip }}
steps:
- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
# Need history to inspect the previous commit message for
# loop detection.
fetch-depth: 2
# persist-credentials lets the final `git push` reuse the
# GITHUB_TOKEN this job was issued.
persist-credentials: true
- name: Decide whether to bump
id: gate
shell: bash
run: |
set -euo pipefail
MSG=$(git log -1 --pretty=%B)
if echo "$MSG" | grep -q '\[skip release\]'; then
echo "Commit carries [skip release]; not bumping."
echo "skip=true" >> "$GITHUB_OUTPUT"
elif echo "$MSG" | grep -q '^chore: bump version to '; then
# The previous push WAS an auto-bump. GitHub's rule about
# GITHUB_TOKEN-pushes not triggering workflows should have
# already broken any loop, but this is a safety net.
echo "Previous commit was an auto-bump; not re-bumping."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Configure git author
if: steps.gate.outputs.skip == 'false'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Bump patch version
id: bump
if: steps.gate.outputs.skip == 'false'
shell: bash
run: |
set -euo pipefail
OLD=$(grep -E '^version *= *"' pyproject.toml | head -1 \
| sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "$OLD" ]; then
echo "::error::could not read current version from pyproject.toml"
exit 1
fi
# Split X.Y.Z and increment Z. If the version has more or
# fewer components, fail loudly rather than guess.
if ! [[ "$OLD" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "::error::version $OLD is not a plain X.Y.Z — pre-release tags require manual bump"
exit 1
fi
X="${BASH_REMATCH[1]}"
Y="${BASH_REMATCH[2]}"
Z="${BASH_REMATCH[3]}"
NEW="$X.$Y.$((Z+1))"
sed -i "s|^version = \"$OLD\"|version = \"$NEW\"|" pyproject.toml
echo "Bumped $OLD → $NEW"
echo "version=$NEW" >> "$GITHUB_OUTPUT"
- name: Commit + push bump
id: push
if: steps.gate.outputs.skip == 'false'
shell: bash
run: |
set -euo pipefail
git add pyproject.toml
git commit -m "chore: bump version to ${{ steps.bump.outputs.version }}"
git push origin HEAD:main
# Capture the SHA of the bump commit so downstream jobs build
# exactly that revision.
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
publish-pypi:
name: Build + publish to PyPI
needs: bump-version
if: needs.bump-version.outputs.skipped == 'false'
runs-on: ubuntu-latest
# No `environment:` block on purpose. Attaching one would make
# GitHub categorise the publish as a Deployment and surface it
# under the repo's "Deployments" sidebar widget. The artefact
# of a successful run is a GitHub Release + a PyPI version —
# both already have first-class UI in their respective places —
# so the Deployment view is redundant noise on the repo home.
steps:
- name: Checkout the bumped commit
uses: actions/checkout@v4
with:
ref: ${{ needs.bump-version.outputs.sha }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Install build backend
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build sdist + wheel
run: python -m build
- name: Verify distribution metadata
run: python -m twine check dist/*
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: python -m twine upload --non-interactive dist/*
create-draft-release:
name: Create draft GitHub release
needs: [bump-version, publish-pypi]
runs-on: ubuntu-latest
steps:
- name: Checkout the bumped commit
uses: actions/checkout@v4
with:
ref: ${{ needs.bump-version.outputs.sha }}
- name: Create draft release
# Draft now, attach Nuitka assets in subsequent jobs, unmark
# draft once everything's uploaded — so consumers never see a
# half-finished release.
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.bump-version.outputs.version }}
name: v${{ needs.bump-version.outputs.version }}
draft: true
generate_release_notes: true
target_commitish: ${{ needs.bump-version.outputs.sha }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-nuitka:
name: Nuitka build (windows-x86_64)
needs: [bump-version, create-draft-release]
# Windows is the only platform users have asked for an .exe on.
# Linux/macOS users install from PyPI; shipping a Nuitka binary
# there just inflates the release page without serving a real
# use case.
runs-on: windows-latest
# PySide6 cold builds run ~50-70 min on a fresh runner (Qt is
# a huge amount of C++ to link). With cache they're back to 5-10
# min. The cap below covers cold + slowest-case parallel link.
timeout-minutes: 90
env:
ASSET_NAME: thesisagents-windows-x86_64.zip
steps:
- name: Checkout the bumped commit
uses: actions/checkout@v4
with:
ref: ${{ needs.bump-version.outputs.sha }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Cache Nuitka build artefacts
uses: actions/cache@v4
with:
# ~/.nuitka holds Nuitka's own caches; *.build holds the
# generated C sources from the previous run. Caching both
# cuts subsequent builds from ~15 min to ~3 min.
path: |
~/.nuitka
~/.cache/Nuitka
thesisagents.build
thesisagents.dist
key: nuitka-${{ runner.os }}-${{ hashFiles('pyproject.toml') }}-${{ needs.bump-version.outputs.version }}
restore-keys: |
nuitka-${{ runner.os }}-${{ hashFiles('pyproject.toml') }}-
nuitka-${{ runner.os }}-
- name: Install runtime + Nuitka
# Install the mcp + gui extras + the parts of intelligence
# that compile cleanly under Nuitka. pymupdf is EXCLUDED from
# the build venv because its Cython binding for MuPDF generates
# a 2.2M-line C file that trips MSVC's per-file heap cap
# (C1002 in pass 2). pypdf alone covers the runtime PDF-text
# path inside the bundle; users who want pymupdf can still
# `pip install thesisagents[intelligence]` from PyPI.
run: |
python -m pip install --upgrade pip
pip install -e ".[mcp,gui]"
pip install pypdf anthropic
pip install nuitka
- name: Compile with Nuitka
# The source plugins are imported dynamically by name at runtime
# (see thesisagents/fetchers/base.py::load_fetcher), so Nuitka's
# static analysis can't see them. A single
# --include-package=thesisagents force-includes every sub-module
# of the package, including all of thesisagents.sources.*. Since
# the 2026-05 migration the plugins live INSIDE the package
# (thesisagents/sources/), so there is no per-source flag list,
# no sources/ data dir, and no PYTHONPATH=sources to set.
#
# python-pptx imports as `pptx` (PyPI name -> module name
# mismatch is normal); use the module name here.
#
# PySide6 is handled ENTIRELY by --enable-plugin=pyside6 —
# the plugin includes the full QML / translations / resources
# tree by default, which is what we want (future tabs may
# use Qt features the current QtWidgets-only Search/Settings
# do not).
#
# Entry point: --python-flag=-m + bare 'thesisagents' tells
# Nuitka to treat the build like `python -m thesisagents`.
# Passing thesisagents/__main__.py directly used to trip
# the "specify its containing directory" warning and made
# sub-imports inside the package resolve oddly.
#
# Distribution model: --standalone produces an
# `thesisagents.dist/` folder containing the exe + every
# DLL/SO it needs; we then zip the folder and attach the
# zip to the release. Onefile mode is intentionally NOT
# used — it self-extracts to %TEMP% on every launch, which
# adds startup latency and trips antivirus heuristics on
# locked-down corporate machines.
shell: bash
run: |
python -m nuitka \
--standalone \
--python-flag=-m \
--output-filename=thesisagents.exe \
--windows-icon-from-ico=assets/icon.ico \
--include-package=thesisagents \
--enable-plugin=pyside6 \
--nofollow-import-to=pymupdf \
--nofollow-import-to=uvicorn \
--nofollow-import-to=fastapi \
--nofollow-import-to=starlette \
--nofollow-import-to=websockets \
--nofollow-import-to=streamlit \
--nofollow-import-to=tornado \
--lto=no \
--jobs=2 \
--include-package-data=pptx \
--include-package-data=openpyxl \
--assume-yes-for-downloads \
thesisagents
- name: Smoke-test the built executable
# If the binary fails to load any of its bundled plugins, this
# surfaces it immediately rather than at the user's first run.
shell: bash
run: |
./thesisagents.dist/thesisagents.exe --version || true
./thesisagents.dist/thesisagents.exe --help > /dev/null
- name: Zip the dist folder
# Compress-Archive ships with PowerShell on every Windows
# runner and produces a deterministic zip. We zip the
# CONTENTS of thesisagents.dist/ rather than the folder
# itself so unzipping does not nest the binary under an
# extra directory layer.
shell: pwsh
run: |
Compress-Archive -Path thesisagents.dist\* `
-DestinationPath $env:ASSET_NAME `
-CompressionLevel Optimal
- name: Compute SHA-256 checksum
# Attach the checksum file alongside the zip so users can
# verify what they downloaded matches what CI built.
shell: bash
run: sha256sum "$ASSET_NAME" > "$ASSET_NAME.sha256"
- name: Attach binary + checksum to release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.bump-version.outputs.version }}
files: |
${{ env.ASSET_NAME }}
${{ env.ASSET_NAME }}.sha256
# The release was created as a draft above; uploading does
# not unmark it.
draft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-release:
name: Mark release as published
needs: [bump-version, build-nuitka]
runs-on: ubuntu-latest
steps:
- name: Unmark draft (publish the release)
uses: actions/github-script@v7
with:
script: |
const tag = "v${{ needs.bump-version.outputs.version }}";
const { owner, repo } = context.repo;
// getReleaseByTag returns 404 for DRAFT releases — per
// https://docs.github.com/rest/releases/releases#get-a-release-by-tag-name
// "You cannot get a draft release by its tag name."
// Enumerate via listReleases (which DOES include drafts)
// and filter by tag name instead.
const releases = await github.paginate(
github.rest.repos.listReleases,
{ owner, repo, per_page: 100 },
);
const release = releases.find(r => r.tag_name === tag);
if (!release) {
throw new Error(
`No release found with tag ${tag}. Existing: ` +
releases.map(r => r.tag_name).join(", "),
);
}
await github.rest.repos.updateRelease({
owner, repo,
release_id: release.id,
draft: false,
});
console.log(`Released ${tag} (${release.html_url})`);