Repository navigation
308 lines (269 loc) · 13.3 KB
/
Copy pathrelease.yml
File metadata and controls
308 lines (269 loc) · 13.3 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
name: Release
on:
push:
tags:
- 'v*.*.*'
# Least privilege: the jobs only need to create/upload/edit GitHub releases.
permissions:
contents: write
jobs:
# The tag names the release and package.json is what electron-builder stamps
# into every artifact name and into the updater feed, so a mismatch publishes a
# build that disagrees with its own tag. Gating here means a bad tag fails
# before `create-draft` runs, so it leaves no orphan draft behind either.
guard:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
# GITHUB_REF/GITHUB_REF_NAME identify the pushed tag, so this needs no tag
# lookup and no full history; it reads package.json and nothing else.
- name: Tag matches package.json
run: npm run check:version
# Creates the release as a draft BEFORE the platform builds, so both matrix
# jobs upload into the same release. Without this, macos and windows run
# concurrently, each finds no release for the tag and each creates its own
# draft: the v1.2.2 attempt produced two releases both tagged v1.2.2, with the
# assets split between them (the .dmg in one, latest.yml in the other), so
# promoting either one would have published an incomplete release with a
# broken update feed.
#
# Drafts left over for this tag are deleted first: a draft is never visible to
# users and can only be the remains of an earlier failed attempt, so mixing
# its stale assets into a fresh run is worse than rebuilding the release
# empty. Re-running a failed release run therefore starts from a clean slate.
create-draft:
needs: guard
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Create the draft release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
gh api "repos/$GITHUB_REPOSITORY/releases" --paginate \
--jq ".[] | select(.draft and .tag_name == \"$TAG\") | .id" \
| while IFS= read -r id; do
echo "deleting stale draft release $id for $TAG"
gh api -X DELETE "repos/$GITHUB_REPOSITORY/releases/$id"
done
gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --draft --title "$TAG" \
--notes "Release notes are generated when this draft is promoted."
release:
needs: create-draft
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, windows-latest]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
# Caches the npm download cache (not node_modules). Requires the
# committed package-lock.json, which `npm ci` verifies against.
cache: 'npm'
# npm, not pnpm, is deliberate: electron-builder's node-module collector is
# built around npm's node_modules layout and silently dropped transitive
# production dependencies under pnpm's virtual store. When the collector
# does miss something, `allowMissingDependencies: false` in
# electron-builder.yml now fails the build rather than shipping a broken
# app. See .npmrc and the `//dependencies` note in package.json.
- name: Install dependencies
run: npm ci
# Fail BEFORE anything is published if the entitlement that lets us load
# third-party native modules is missing from the plist.
- name: Verify macOS entitlements (pre-build gate)
if: matrix.os == 'macos-latest'
run: |
python3 - <<'PY'
import plistlib, sys
path = 'build/entitlements.mac.plist'
try:
ents = plistlib.load(open(path, 'rb'))
except Exception as exc:
print(f'::error::{path} is not a readable plist: {exc}')
sys.exit(1)
# keep-sorted start
required = [
'com.apple.security.cs.allow-jit',
'com.apple.security.cs.allow-unsigned-executable-memory',
'com.apple.security.cs.disable-library-validation',
]
# keep-sorted end
missing = [k for k in required if ents.get(k) is not True]
if missing:
print('::error::' + path + ' is missing required entitlements: ' + ', '.join(missing))
print('::error::Without disable-library-validation the app crashes at launch')
print('::error::with "mapping process and mapped file have different Team IDs".')
sys.exit(1)
print('OK - entitlements present:', ', '.join(sorted(ents)))
PY
- name: Build application
run: npm run build
# macOS 代码签名(可选)
- name: Import Code Signing Certificate (macOS)
if: matrix.os == 'macos-latest'
env:
CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE }}
CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
run: |
if [ -n "$CERTIFICATE_P12" ]; then
echo "$CERTIFICATE_P12" | base64 --decode > certificate.p12
security create-keychain -p actions build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p actions build.keychain
security import certificate.p12 -k build.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k actions build.keychain
security find-identity -v -p codesigning build.keychain
else
echo "::warning title=No Developer ID certificate::MACOS_CERTIFICATE is empty, so this build will be AD-HOC SIGNED and NOT notarized. It will not launch on other people's Macs until users run: sudo xattr -rd com.apple.quarantine /Applications/KnowNote.app"
fi
# Windows 代码签名(可选)
- name: Code Sign (Windows)
if: matrix.os == 'windows-latest'
env:
CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
if ($env:CERTIFICATE) {
echo "Windows 代码签名已配置"
}
# 构建和发布
# --arm64 is explicit so the produced architecture does not silently depend
# on whatever CPU the macos-latest runner happens to have.
- name: Build & Release (macOS)
if: matrix.os == 'macos-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: npm run build:mac -- --arm64 --publish always
# Inspect the artifact we just shipped. This is the check that was missing
# when v1.2.0 went out ad-hoc signed, hardened-runtime, and unable to load
# its native module.
- name: Verify macOS artifact (signature, entitlements, native modules)
if: matrix.os == 'macos-latest'
run: |
set -uo pipefail
APP=$(find dist -maxdepth 2 -type d -name 'KnowNote.app' | head -n1)
if [ -z "$APP" ]; then
echo "::error::no KnowNote.app was produced under dist/"
exit 1
fi
echo "Artifact: $APP"
echo "--- main binary signature ---"
codesign -dv --verbose=4 "$APP" 2>&1 | grep -E 'Identifier|TeamIdentifier|Signature|flags' || true
TEAM=$(codesign -dv "$APP" 2>&1 | sed -n 's/^TeamIdentifier=//p')
case "$TEAM" in
''|'not set')
IS_ADHOC=1
echo "::warning title=Unsigned macOS build::No Developer ID TeamIdentifier. Users must run: sudo xattr -rd com.apple.quarantine /Applications/KnowNote.app (see the Install section of README.md)"
;;
*)
IS_ADHOC=0
echo "Developer ID TeamIdentifier: $TEAM"
;;
esac
echo "--- entitlements actually embedded in the shipped binary ---"
ENT=$(mktemp)
codesign -d --entitlements :- "$APP" > "$ENT" 2>/dev/null || true
cat "$ENT"
# An unsigned bundle embeds no entitlements, and that is the expected
# state while the pipeline has no Developer ID certificate: see the
# Install section of README.md for the user-side workaround. It is only
# tolerable without an identity though - once a certificate is present,
# missing entitlements mean the v1.2.0 crash is back and the release
# must not go out.
if grep -q 'disable-library-validation' "$ENT"; then
echo "entitlements: disable-library-validation is present"
elif [ "$IS_ADHOC" -eq 1 ]; then
echo "::warning title=Unsigned macOS build::No identity was available, so the bundle carries no entitlements. This build is not notarized and cannot be launched on Apple Silicon until the user clears the quarantine flag; README.md documents the command."
else
echo "::error::shipped app is missing com.apple.security.cs.disable-library-validation."
echo "::error::It will crash on launch when the main process loads better-sqlite3's native module."
exit 1
fi
echo "--- native modules unpacked from the asar ---"
NATIVES=$(find "$APP/Contents/Resources/app.asar.unpacked" -type f \( -name '*.node' -o -name '*.dylib' \) 2>/dev/null)
if [ -z "$NATIVES" ]; then
echo "::error::no native modules found in app.asar.unpacked - asarUnpack is broken"
exit 1
fi
echo "$NATIVES" | while IFS= read -r f; do
printf '%s\n -> ' "$f"
codesign -dv "$f" 2>&1 | grep -E 'TeamIdentifier|Signature' | tr '\n' ' ' || echo -n 'NO SIGNATURE'
echo
done
# better-sqlite3 >= 13 is N-API and ships its binary as
# `prebuilds/<platform>-<arch>.node`, not the old
# `build/Release/better_sqlite3.node`, and nothing rebuilds it.
if ! echo "$NATIVES" | grep -qE 'better-sqlite3/prebuilds/.+\.node'; then
echo "::error::a better-sqlite3 prebuild was not unpacked; the database will fail to open"
exit 1
fi
echo "--- consistency ---"
VERIFY_FAILED=0
codesign --verify --strict --verbose=2 "$APP" || VERIFY_FAILED=1
echo "$NATIVES" | while IFS= read -r f; do
codesign --verify --strict "$f" || echo "::warning::$f failed signature verification"
done
if [ "$VERIFY_FAILED" -eq 1 ]; then
if [ "$IS_ADHOC" -eq 0 ]; then
echo "::error::codesign --verify failed on a Developer ID signed bundle."
echo "::error::This build would be rejected on user machines."
exit 1
fi
echo "::warning title=Unsigned macOS build::codesign --verify --strict failed, which is expected without an identity. The app launches only after the user clears the quarantine flag; inspect the log above before publishing."
else
echo "signature verify: OK"
fi
if [ "$IS_ADHOC" -eq 1 ]; then
echo "::notice title=macOS artifact verified::$(echo "$NATIVES" | wc -l | tr -d ' ') native module(s) unpacked; bundle is unsigned."
else
echo "::notice title=macOS artifact verified::entitlements present, $(echo "$NATIVES" | wc -l | tr -d ' ') native module(s) unpacked and signed."
fi
- name: Build & Release (Windows)
if: matrix.os == 'windows-latest'
timeout-minutes: 30
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build:win -- --publish always
# electron-builder uploads into the draft created by `create-draft` (see
# publish.releaseType in electron-builder.yml, and the note on that job for
# why the draft is created up front). It is promoted to public here, which
# happens only if every platform built and the macOS artifact verification
# passed. A broken package therefore never reaches users - it stays a draft.
publish:
needs: release
runs-on: ubuntu-latest
steps:
- name: Promote draft release to public
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft' | grep -qx true || {
echo "::error::no draft release found for $GITHUB_REF_NAME"; exit 1; }
# Release notes are generated from the labels on the merged PRs, per
# .github/release.yml. `gh release edit` has no --generate-notes, so
# the API endpoint is used and its body written back to the draft.
NOTES=$(gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" \
-f tag_name="$GITHUB_REF_NAME" --jq '.body')
if [ -z "$NOTES" ]; then
echo "::error::generated release notes for $GITHUB_REF_NAME are empty"
exit 1
fi
gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" \
--notes "$NOTES" --draft=false --latest
echo "::notice title=Published::Released $GITHUB_REF_NAME"