-
Notifications
You must be signed in to change notification settings - Fork 0
284 lines (275 loc) · 12.9 KB
/
Copy pathrelease.yml
File metadata and controls
284 lines (275 loc) · 12.9 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
name: Release
# Manual only. A push to master is not a release; a release is a decision.
on:
workflow_dispatch:
inputs:
bump:
description: 'Version bump, e.g. +0.1 or +1.0. Empty = build number only.'
type: string
required: false
default: ''
# Read by default; the three jobs that write say so themselves. `build` in particular must not
# hold a writable token: it compiles the release and runs a downloaded packaging tool.
permissions:
contents: read
concurrency: release
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
contents: write # pushes the release branch
outputs:
version: ${{ steps.v.outputs.version }}
base: ${{ steps.v.outputs.base }}
branch: ${{ steps.v.outputs.branch }}
bumped: ${{ steps.v.outputs.bumped }}
steps:
- uses: actions/checkout@v7
- id: v
name: Compute version
env:
BUMP: ${{ inputs.bump }} # via env, never interpolated into the script
run: |
set -euo pipefail
IFS=. read -r MAJOR MINOR < VERSION
bumped=false
if [ -n "$BUMP" ]; then
if ! printf '%s' "$BUMP" | grep -Eq '^\+?[0-9]+\.[0-9]+$'; then
echo "::error::bump must look like +0.1 or +1.0 (got '$BUMP')"
exit 1
fi
IFS=. read -r DMAJOR DMINOR <<< "${BUMP#+}"
if [ "$DMAJOR" -gt 0 ]; then
# A major bump restarts the minor rather than carrying it: +1.0 off 0.7 is 1.0.
MAJOR=$((MAJOR + DMAJOR)); MINOR="$DMINOR"
else
MINOR=$((MINOR + DMINOR))
fi
printf '%s.%s\n' "$MAJOR" "$MINOR" > VERSION
bumped=true
fi
ver="${MAJOR}.${MINOR}.${{ github.run_number }}"
{
echo "version=${ver}"
echo "base=${MAJOR}.${MINOR}"
echo "branch=release/v${ver}"
echo "bumped=${bumped}"
} >> "$GITHUB_OUTPUT"
echo "Releasing v${ver} (bumped: ${bumped})"
- name: Push release branch
env:
BRANCH: ${{ steps.v.outputs.branch }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
# Nothing to commit when only the build number moved; the branch is then just a tag target.
if [ "${{ steps.v.outputs.bumped }}" = "true" ]; then
git commit -am "chore: version ${{ steps.v.outputs.base }}"
fi
git push origin "$BRANCH"
build:
needs: prepare
strategy:
fail-fast: false
matrix:
include:
# The oldest image GitHub still offers, because the binary's glibc floor is whatever it
# was built against: 2.35 here, against 2.39 on ubuntu-latest, which will not start on
# Debian 12 or Ubuntu 22.04. The AppImage bundles libcurl and OpenSSL but never glibc.
- os: ubuntu-22.04
name: linux-x64
- os: windows-latest
name: win64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.prepare.outputs.branch }}
# Nothing here talks to git again, and checkout otherwise leaves a usable token on
# disk (under `$RUNNER_TEMP` since v6) for anything running in the job to read.
persist-credentials: false
- name: Install Linux deps
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxfixes-dev \
libxss-dev libxtst-dev libxrender-dev libxinerama-dev \
libcurl4-openssl-dev zlib1g-dev \
libxkbcommon-dev libwayland-dev wayland-protocols libgl1-mesa-dev libegl1-mesa-dev \
libasound2-dev libpulse-dev libdbus-1-dev libudev-dev
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DAPP_BUILD=${{ github.run_number }}
- name: Build
run: cmake --build build --config Release
- name: Package
shell: bash
run: |
mkdir -p dist
ver="${{ needs.prepare.outputs.version }}"
if [ "${{ runner.os }}" = "Windows" ]; then
cp build/Release/PathOfPriceCheck.exe dist/
(cd dist && 7z a "PathOfPriceCheck-${ver}-${{ matrix.name }}.zip" PathOfPriceCheck.exe)
# The same executable, unwrapped. The updater applies a *file*, and nothing in the
# binary reads a .zip or a .tar.gz container — see docs/updater.md.
mv "dist/PathOfPriceCheck.exe" "dist/PathOfPriceCheck-${ver}-${{ matrix.name }}.exe"
else
cp build/PathOfPriceCheck dist/
(cd dist && tar czf "PathOfPriceCheck-${ver}-${{ matrix.name }}.tar.gz" PathOfPriceCheck)
mv "dist/PathOfPriceCheck" "dist/PathOfPriceCheck-${ver}-${{ matrix.name }}"
fi
# The recommended Windows download. A per-user install lands somewhere writable, which is
# what keeps the updater working; the .zip above stays for whoever wants a movable folder.
# PowerShell, not bash: Git Bash rewrites any argument that starts with a '/' into a
# Windows path, so ISCC's own /D switches arrive as filenames and it refuses the lot.
# CI compiles this same script on every PR, so a break here is caught before a release.
- name: Package installer
if: runner.os == 'Windows'
shell: pwsh
env:
VER: ${{ needs.prepare.outputs.version }}
run: |
$iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
if (-not (Test-Path $iscc)) { choco install innosetup -y --no-progress }
& $iscc /Qp `
"/DAppVersion=${env:VER}" `
"/DSourceExe=..\build\Release\PathOfPriceCheck.exe" `
"/DOutDir=..\dist" `
"/DOutName=PathOfPriceCheck-${env:VER}-win64-setup" `
packaging\PathOfPriceCheck.iss
if ($LASTEXITCODE -ne 0) { exit 1 }
# SDL3 is static but libcurl, OpenSSL and libstdc++ are the system's, so the bare binary
# assumes a distribution close to this runner. The AppImage carries them; libX11 and libGL
# stay the host's, as they must.
#
# linuxdeploy is pinned by tag and verified by digest: `continuous` is a mutable tag, and
# what it serves is executed over the release payload — the whole supply chain behind the
# AppImage a user installs. Both pins were checked against `continuous` on this runner
# image and package the same thing; bump them deliberately, never silently.
- name: Package AppImage
if: runner.os == 'Linux'
env:
OUTPUT: PathOfPriceCheck-${{ needs.prepare.outputs.version }}-linux-x64.AppImage
APPIMAGE_EXTRACT_AND_RUN: 1 # the runners have no FUSE, so every AppImage here unpacks itself
LD_TAG: 1-alpha-20251107-1
LD_SHA: c20cd71e3a4e3b80c3483cef793cda3f4e990aca14014d23c544ca3ce1270b4d
PLUGIN_TAG: 1-alpha-20250213-1
PLUGIN_SHA: 992d502a248e14ab185448ddf6f6e7d25558cb84d4623c354c3af350c25fccb3
run: |
set -euo pipefail
base=https://github.com/linuxdeploy
curl -fsSLO "$base/linuxdeploy/releases/download/$LD_TAG/linuxdeploy-x86_64.AppImage"
curl -fsSLO "$base/linuxdeploy-plugin-appimage/releases/download/$PLUGIN_TAG/linuxdeploy-plugin-appimage-x86_64.AppImage"
printf '%s %s\n' \
"$LD_SHA" linuxdeploy-x86_64.AppImage \
"$PLUGIN_SHA" linuxdeploy-plugin-appimage-x86_64.AppImage | sha256sum -c -
chmod +x linuxdeploy*.AppImage
./linuxdeploy-x86_64.AppImage --appdir AppDir \
-e build/PathOfPriceCheck \
-d assets/popc.desktop \
-i assets/popc_icon.png \
--output appimage
mv "$OUTPUT" dist/
- uses: actions/upload-artifact@v7
with:
name: ${{ matrix.name }}
path: dist/PathOfPriceCheck-*
if-no-files-found: error
release:
needs: [prepare, build]
runs-on: ubuntu-latest
permissions:
contents: write # creates the release
steps:
- uses: actions/download-artifact@v8
with:
path: artifacts
merge-multiple: true
# What the running application checks. Published as a release asset rather than read out
# of the API, so the client can use the fixed `releases/latest/download/` URL and never
# spend one of the 60 unauthenticated API calls an hour a shared address gets.
- name: Build latest.json
env:
VER: ${{ needs.prepare.outputs.version }}
BASE: ${{ github.server_url }}/${{ github.repository }}
run: |
set -euo pipefail
for f in artifacts/*; do
n=$(basename "$f")
jq -n --arg name "$n" \
--arg url "${BASE}/releases/download/v${VER}/${n}" \
--arg sha "$(sha256sum "$f" | cut -d' ' -f1)" \
--argjson size "$(stat -c%s "$f")" \
'{name: $name, url: $url, sha256: $sha, size: $size}'
done | jq -s --arg ver "$VER" --arg notes "${BASE}/releases/tag/v${VER}" \
'{schema_version: 1, version: $ver, notes_url: $notes, assets: .}' \
> latest.json
mv latest.json artifacts/
cat artifacts/latest.json
- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
VER: ${{ needs.prepare.outputs.version }}
BRANCH: ${{ needs.prepare.outputs.branch }}
run: |
set -euo pipefail
# `--generate-notes` is a list of PR *titles*, and a title is the topic rather than the
# change. What a user needs is each PR's **Release notes** section — the new, changed
# and removed ways to use the app — so the same list is asked for through the API and
# every PR it names is opened for that one section. The generated list keeps its place
# underneath, where it is the index and the attribution rather than the notes.
notes=$(gh api "repos/${GH_REPO}/releases/generate-notes" \
-f tag_name="v${VER}" -f target_commitish="${BRANCH}" -q .body)
{
# Ascending, and de-duplicated: a PR is linked twice when its author is also a new
# contributor. Numbers come off the links in the notes rather than off the commits,
# so whatever GitHub decided is in this release is what gets read.
for n in $(printf '%s' "$notes" | grep -oE 'pull/[0-9]+' | cut -d/ -f2 | sort -un); do
# Everything under "## Release notes" until the next heading. A PR with nothing
# user-facing has no such section and contributes nothing here, which is the
# answer for a refactor or a version bump rather than an omission.
section=$(gh pr view "$n" --json body -q .body | awk '
{ sub(/\r$/, "") }
/^##[[:space:]]/ { keep = ($0 ~ /^##[[:space:]]*[Rr]elease[[:space:]]+[Nn]otes[[:space:]]*$/); next }
keep')
if [ -n "$(printf '%s' "$section" | tr -d '[:space:]')" ]; then
printf '%s\n\n' "$section"
fi
done
printf -- '---\n\n%s\n' "$notes"
} | sed '/^🤖 Generated with/d' | cat -s | sed '/./,$!d' > notes.md
gh release create "v${VER}" artifacts/* --title "v${VER}" \
--target "${BRANCH}" --notes-file notes.md
# The site's download buttons name this release's assets, so it is republished once they exist.
pages:
needs: release
permissions:
contents: read
pages: write
id-token: write
uses: ./.github/workflows/pages.yml
# Land the version bump on master, or clean up a branch nothing will ever merge.
finalize:
needs: [prepare, release]
if: always() && needs.prepare.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: write # deletes the release branch
pull-requests: write # opens the version PR
steps:
- uses: actions/checkout@v7
- name: Open version PR
if: needs.release.result == 'success' && needs.prepare.outputs.bumped == 'true'
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
gh pr create --base master --head "${{ needs.prepare.outputs.branch }}" \
--title "chore: version ${{ needs.prepare.outputs.base }}" \
--body "Released as [v${{ needs.prepare.outputs.version }}](${{ github.server_url }}/${{ github.repository }}/releases/tag/v${{ needs.prepare.outputs.version }})."
- name: Delete release branch
if: needs.release.result != 'success' || needs.prepare.outputs.bumped != 'true'
run: git push origin --delete "${{ needs.prepare.outputs.branch }}"