-
Notifications
You must be signed in to change notification settings - Fork 0
274 lines (257 loc) · 13.4 KB
/
Copy pathrelease.yml
File metadata and controls
274 lines (257 loc) · 13.4 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
name: Release
# release-please watches conventional commits on `main` and maintains a "release PR"
# that bumps the version (app/package.json + Cargo.toml + tauri.conf.json) and the
# changelog. Merging that PR cuts the GitHub release + tag; the build matrix then
# builds + signs each platform's bundles and the latest-json job aggregates a signed
# latest.json onto the release. workflow_dispatch with a `tag` rebuilds an existing
# release's assets (recovery / manual fill-in); without one it's a build-only smoke
# test.
on:
push:
branches: [main]
workflow_dispatch:
inputs:
tag:
description: "Existing release tag to build + attach assets to (e.g. v0.2.0). Empty = build-only smoke test."
required: false
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
# The one package is rooted at the repo (path "."), so the action emits
# unprefixed outputs. `releases_created` is the top-level "anything released" flag.
outputs:
release_created: ${{ steps.rp.outputs.releases_created }}
tag_name: ${{ steps.rp.outputs.tag_name }}
steps:
- uses: googleapis/release-please-action@v4
id: rp
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
# Rewrite the fresh release's body with an LLM: a short friendly summary on
# top, the raw release-please changelog kept verbatim below a divider.
# Sequenced here — in the same job, before the build matrix — so it always
# finishes before latest-json copies the body into latest.json's `notes`
# (the in-app update dialog renders that markdown); a separate
# `release: published` workflow would race it. Fails open at every step: a
# missing OPENROUTER_API_KEY secret, an API error, or an empty reply keeps
# the raw notes and the workflow green. The model only writes the summary —
# the script assembles summary + divider + original body itself, so the
# changelog can't be dropped or altered, and CHANGELOG.md is never touched.
- name: Polish release notes with AI
if: ${{ steps.rp.outputs.releases_created == 'true' }}
continue-on-error: true
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
MODEL: ${{ vars.RELEASE_NOTES_MODEL || 'anthropic/claude-3.5-haiku' }}
TAG: ${{ steps.rp.outputs.tag_name }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
if [ -z "$OPENROUTER_API_KEY" ]; then
echo "OPENROUTER_API_KEY secret not set; keeping the raw release notes"
exit 0
fi
BODY="$(gh release view "$TAG" --repo "$REPO" --json body -q .body)"
if [ -z "$BODY" ]; then
echo "release body is empty; nothing to polish"
exit 0
fi
SYSTEM='You write release notes for PyOps, a desktop factory planner and
in-game assistant for Factorio (Pyanodons). You are given the auto-generated
changelog for one release: grouped conventional-commit subjects with commit
links. Rewrite it as a short, friendly release summary in Markdown: one or
two plain sentences saying what this release is about, then a few themed
highlight bullets (group related commits; lead with what users notice).
Strict rules: only describe changes that appear in the given changelog —
never invent, embellish, or speculate about features or fixes. Do not
include a version heading, a date, the word "Changelog", commit hashes, or
links. Skip pure chore/CI noise unless it is all there is. Output plain
Markdown only — no code fence around the output, no preamble like "Here is
the summary".'
REQ="$(jq -n --arg model "$MODEL" --arg sys "$SYSTEM" --arg body "$BODY" \
'{model: $model, max_tokens: 1200,
messages: [{role: "system", content: $sys}, {role: "user", content: $body}]}')"
RESP="$(curl -sS --max-time 120 https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d "$REQ")" || { echo "OpenRouter request failed; keeping the raw release notes"; exit 0; }
SUMMARY="$(jq -r '.choices[0].message.content // empty' <<<"$RESP" 2>/dev/null || true)"
if [ -z "$SUMMARY" ]; then
echo "no summary in the OpenRouter response; keeping the raw release notes"
jq -r '.error.message // empty' <<<"$RESP" 2>/dev/null || true
exit 0
fi
printf '%s\n\n---\n\n%s\n' "$SUMMARY" "$BODY" > polished-notes.md
gh release edit "$TAG" --repo "$REPO" --notes-file polished-notes.md
echo "release notes polished with $MODEL"
build:
needs: release-please
if: ${{ needs.release-please.outputs.release_created == 'true' || github.event_name == 'workflow_dispatch' }}
strategy:
fail-fast: false
matrix:
include:
# `updater` is the glob for this platform's updater artifact — the one that
# goes in latest.json (deb/dmg are install-only and also produce .sig files,
# so pick explicitly rather than grab the first .sig).
- platform: ubuntu-22.04 # Linux x64 (deb + AppImage)
bundles: "deb,appimage"
target: linux-x86_64
updater: "*.AppImage"
- platform: macos-14 # macOS Apple Silicon (app.tar.gz = updater artifact)
bundles: "app,dmg"
target: darwin-aarch64
updater: "*.app.tar.gz"
- platform: macos-14 # macOS Intel — cross-compiled on arm64, since the
bundles: "app,dmg" # macos-13 (Intel) runners are deprecated and badly
target: darwin-x86_64 # queue-starved. arm64 runners are plentiful.
updater: "*.app.tar.gz"
rust_target: x86_64-apple-darwin
- platform: windows-latest # Windows x64
bundles: "nsis"
target: windows-x86_64
updater: "*-setup.exe"
runs-on: ${{ matrix.platform }}
steps:
# Always build the workflow's own ref (the release commit on push, main on a
# manual dispatch) — never an old tag, whose tree predates later CI fixes like
# vendor-node.sh's cross-compile support. main carries the just-released version
# until the next release PR, so a tag rebuild still yields that version's bundles.
- uses: actions/checkout@v4
# Tauri's webview + bundling deps (Linux only). The Ubuntu runner is Debian-
# based, so unlike an Arch host its linuxdeploy/AppImage tooling works.
- name: Install Linux build dependencies
if: startsWith(matrix.platform, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev \
libayatana-appindicator3-dev patchelf file
# Rust toolchain for the Tauri shell.
- uses: dtolnay/rust-toolchain@stable
- uses: swatinem/rust-cache@v2
with:
workspaces: app/src-tauri
# Cross-compile target (Intel macOS on the arm64 runner); no-op otherwise.
- name: Add the cross-compile Rust target
if: ${{ matrix.rust_target != '' }}
run: rustup target add ${{ matrix.rust_target }}
# vite-plus toolchain + app deps (same as ci.yml).
- uses: voidzero-dev/setup-vp@v1
with:
node-version: "24"
cache: true
- name: Install app dependencies
working-directory: app
run: vp install
# Vendor the node sidecar (gitignored, so it isn't in the checkout). For a
# cross-compile it must match the *target* arch, not the runner's host arch.
- name: Vendor the node sidecar
working-directory: app/src-tauri
shell: bash
run: TARGET_TRIPLE="${{ matrix.rust_target }}" ./vendor-node.sh
# The release tag this run targets: release-please's tag on a push, or the
# dispatch input on a manual rebuild. Empty on a build-only smoke test.
- name: Resolve release tag
id: tag
shell: bash
run: echo "tag=${{ needs.release-please.outputs.tag_name || inputs.tag }}" >> "$GITHUB_OUTPUT"
# Build the bundles and sign the updater artifacts. createUpdaterArtifacts (in
# tauri.conf.json) emits the updater bundle + a .sig per platform. We run the
# CLI directly rather than via tauri-action, which assumes pnpm — our toolchain
# is vp (it runs as beforeBuildCommand). `shell: bash` resolves the .bin shim
# on every OS, including Windows.
- name: Build and sign
working-directory: app
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
target_arg=""
[ -n "${{ matrix.rust_target }}" ] && target_arg="--target ${{ matrix.rust_target }}"
./node_modules/.bin/tauri build $target_arg --bundles ${{ matrix.bundles }}
# Both macOS arches emit `PyOps.app.tar.gz` (no arch in the name), so they'd
# clobber each other on upload. Arch-suffix it (and its .sig) so each is
# distinct. The .app.tar.gz is updater-only (users download the .dmg), so this
# rename is invisible to them.
- name: Disambiguate the macOS updater artifact
if: ${{ steps.tag.outputs.tag != '' && startsWith(matrix.target, 'darwin') }}
shell: bash
run: |
f=$(find app/src-tauri/target -path '*/release/bundle/*' -name '*.app.tar.gz' -type f | head -1)
if [ -z "$f" ]; then echo "::error::no .app.tar.gz produced"; exit 1; fi
base="${f%.app.tar.gz}"
mv "$f" "${base}_${{ matrix.target }}.app.tar.gz"
mv "$f.sig" "${base}_${{ matrix.target }}.app.tar.gz.sig"
# Attach the install bundles + updater artifacts (+ their .sig) to the release.
# `while read` (not mapfile) so it works on the macOS runners' bash 3.2.
- name: Upload bundles to the release
if: ${{ steps.tag.outputs.tag != '' }}
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
files=()
while IFS= read -r -d '' f; do files+=("$f"); done < <(
find app/src-tauri/target -path '*/release/bundle/*' -type f \
\( -name '*.deb' -o -name '*.AppImage' -o -name '*.dmg' \
-o -name '*-setup.exe' -o -name '*.app.tar.gz' -o -name '*.sig' \) -print0)
printf 'uploading: %s\n' "${files[@]}"
gh release upload "${{ steps.tag.outputs.tag }}" "${files[@]}" --clobber
# Emit this platform's latest.json fragment (target -> signature + asset URL);
# the aggregate job merges all four into one latest.json.
- name: Build updater fragment
if: ${{ steps.tag.outputs.tag != '' }}
shell: bash
run: |
art=$(find app/src-tauri/target -path '*/release/bundle/*' -name '${{ matrix.updater }}' -type f | head -1)
if [ -z "$art" ]; then echo "::error::no updater artifact (${{ matrix.updater }}) for ${{ matrix.target }}"; exit 1; fi
sig="${art}.sig"
if [ ! -f "$sig" ]; then echo "::error::no .sig beside $art"; exit 1; fi
name=$(basename "$art")
url="https://github.com/${{ github.repository }}/releases/download/${{ steps.tag.outputs.tag }}/${name}"
jq -n --arg t "${{ matrix.target }}" --arg sig "$(cat "$sig")" --arg url "$url" \
'{($t): {signature: $sig, url: $url}}' > "fragment-${{ matrix.target }}.json"
cat "fragment-${{ matrix.target }}.json"
- name: Upload fragment
if: ${{ steps.tag.outputs.tag != '' }}
uses: actions/upload-artifact@v4
with:
name: updater-${{ matrix.target }}
path: fragment-${{ matrix.target }}.json
# Merge the per-platform fragments into one latest.json and attach it. The app's
# updater endpoint is releases/latest/download/latest.json. Runs only when the
# builds targeted a real tag (skipped on a build-only smoke test).
latest-json:
needs: [release-please, build]
if: ${{ (needs.release-please.outputs.release_created == 'true' || github.event_name == 'workflow_dispatch') && (needs.release-please.outputs.tag_name != '' || inputs.tag != '') }}
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: updater-*
merge-multiple: true
path: fragments
- name: Assemble and upload latest.json
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ needs.release-please.outputs.tag_name || inputs.tag }}"
shopt -s nullglob
frags=(fragments/*.json)
if [ ${#frags[@]} -eq 0 ]; then echo "::error::no updater fragments"; exit 1; fi
NOTES="$(gh release view "$TAG" --repo "${{ github.repository }}" --json body -q .body)"
PLATFORMS="$(jq -s 'add' "${frags[@]}")"
jq -n --arg v "${TAG#v}" --arg n "$NOTES" \
--arg d "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --argjson p "$PLATFORMS" \
'{version: $v, notes: $n, pub_date: $d, platforms: $p}' > latest.json
cat latest.json
gh release upload "$TAG" latest.json --repo "${{ github.repository }}" --clobber