Repository navigation
502 lines (458 loc) · 23.9 KB
/
Copy pathrelease.yml
File metadata and controls
502 lines (458 loc) · 23.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
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
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
# Tagged public releases for macOS and Linux.
# Windows is signed on a card and published separately - see the matrix below.
#
# git tag v1.4.0 && git push origin v1.4.0
#
# Builds the stock Posnic app on each platform, attaches the installers to a
# GitHub Release and writes the checksums people need to verify what they
# downloaded. White-label builds are a different pipeline (Posnic/build) and
# deliberately never publish here.
name: Release
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag:
description: "Existing tag to build (for re-running a failed release)"
required: true
permissions:
contents: write # needed to create the release and upload assets
jobs:
# A tag that fails its own tests should never reach a download page.
#
# This used to be a single job running the API unit suite and nothing else.
# A release could therefore ship with drifted API docs, a lint error, or a
# module required at runtime but missing from the packaged app - the check
# that catches that last one is in CI, and a release never ran it. A release
# gate should be at least as strict as a pull request, not weaker than one.
#
# Calling ci.yml rather than copying its jobs means the two cannot drift.
#
# One limit worth knowing: a reusable workflow always runs against the ref
# that triggered it. On the normal path - pushing a v* tag - that is the tag,
# which is what we want. On a workflow_dispatch re-run it is the default
# branch, so the tag's own build is still gated, but by main's tests. Re-run
# the tag push itself if that distinction matters.
verify:
name: Full CI
uses: ./.github/workflows/ci.yml
secrets: inherit
gate:
name: Safe to publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
# verifyUpdateCodeSignature has to stay false while builds are unsigned -
# turning it on with no signature to check makes every update fail. The
# risk is not the flag; it is that the certificate arrives, signing gets
# configured, and nobody remembers this one line. Then the update channel
# is live, public and unverified, and nothing says so.
#
# So a release that would publish public installers fails here unless
# verification is on, or the run is explicitly marked as a deliberate
# unsigned pre-release.
- name: Update trust and version format
env:
POSNIC_ALLOW_UNSIGNED_RELEASE: ${{ vars.POSNIC_ALLOW_UNSIGNED_RELEASE }}
run: node scripts/check-release-gate.js
build:
name: ${{ matrix.name }}
needs: [verify, gate]
strategy:
fail-fast: false
matrix:
# WINDOWS IS NOT BUILT HERE, on purpose.
#
# Its code signing key lives on a Certum card - CA/Browser Forum rules
# have required code signing keys to sit on certified hardware since
# June 2023, so there is no .pfx and no secret to configure - and a
# GitHub-hosted runner has no card reader. A Windows build produced here
# could only ever be UNSIGNED, and shipping that is worse than shipping
# nothing: every customer sees "Unknown publisher".
#
# It is built and published from a desk instead:
# npm run build (with POSNIC_SIGN_SHA1 set)
# npm run release:windows -- <tag>
#
# That command refuses to publish anything unsigned, untimestamped, or
# whose latest.yml does not match the installer beside it.
include:
- { os: macos-latest, name: macOS, flag: '--mac' }
- { os: ubuntu-latest, name: Linux, flag: '--linux --x64' }
runs-on: ${{ matrix.os }}
# secrets cannot appear in step-level `if:` expressions (GitHub refuses
# the whole file, silently, as a 0-second startup failure) - so their
# PRESENCE is computed here, where secrets are legal, and the steps
# branch on these strings instead.
env:
HAVE_MAC_CERT: ${{ secrets.MAC_CERT_P12 != '' }}
HAVE_NOTARY: ${{ secrets.ASC_API_KEY_P8 != '' }}
steps:
- uses: actions/checkout@v7
with:
ref: '${{ inputs.tag || github.ref }}'
submodules: recursive
- uses: actions/setup-node@v7
with: { node-version: 22 }
- name: Install
run: npm ci
# The v1.6.2 tag locks 26.15.3. New macOS runners reject its use of the
# certificate import password to unlock a randomly-passworded keychain.
# Apply the upstream three-line fix without changing the tagged app:
# https://github.com/electron-userland/electron-builder/pull/10172
# Keep this in the workflow so workflow_dispatch can recover older tags.
- name: Correct the legacy macOS keychain password
if: matrix.name == 'macOS' && env.HAVE_MAC_CERT == 'true'
run: |
node <<'NODE'
const fs = require('node:fs');
const file = require.resolve('app-builder-lib/out/codeSign/macCodeSign.js');
let source = fs.readFileSync(file, 'utf8');
const changes = [
['return await importCerts(keychainFile, certPaths, cscPasswords);',
'return await importCerts(keychainFile, certPaths, cscPasswords, keychainPassword);'],
['async function importCerts(keychainFile, paths, keyPasswords) {',
'async function importCerts(keychainFile, paths, keyPasswords, keychainPassword) {'],
['"-s", "-k", password, keychainFile',
'"-s", "-k", keychainPassword, keychainFile'],
];
if (changes.every(([, fixed]) => source.includes(fixed))) {
console.log('The keychain password correction is already present.');
} else {
for (const [before, after] of changes) {
if (source.split(before).length !== 2) {
throw new Error('macOS signing implementation changed; review the keychain workaround.');
}
source = source.replace(before, after);
}
fs.writeFileSync(file, source);
console.log('Corrected the temporary keychain password passed to security.');
}
NODE
# The frontend is a separate package with its own lockfile, and gulp lives
# only in its node_modules. prepare:bundle below runs that gulp build, so
# without this the release build fails on a runner and works on every
# machine that happens to have installed the frontend once.
- name: Install the frontend build toolchain
working-directory: frontend
run: npm ci
# The API's dependencies, because prepare:api-runtime zips them into the
# installer. It wants the full tree rather than a production one: it works
# out which top-level packages are dev-only with `npm ls --omit=dev` and
# drops those itself, so --omit=dev here would leave it unable to tell.
#
# ci.yml has always installed these. release.yml never did, which is why
# every platform failed in prepare:bundle rather than at a build step.
- name: Install the API dependencies
working-directory: api
run: npm ci
# MongoDB ships inside the installer and its binaries are not in git -
# they are 90 MB of someone else's build output. A developer gets them
# from src/download-mongodb.bat; a runner has to fetch them itself, and
# without this the packaged app has no database to start.
# The database, for whichever machine this is.
#
# This step used to run only on Windows, so the macOS and Linux packages
# shipped with no database at all and could not open. Every platform gets
# its own mongod now, from the same pinned version.
#
# macOS builds both architectures from one runner, so both binaries are
# fetched and electron-builder picks per-arch through ${arch} below.
- name: Fetch the bundled MongoDB
shell: bash
run: |
set -euo pipefail
VERSION=$(grep -oE 'set MONGODB_VERSION=[0-9.]+' src/download-mongodb.bat | cut -d= -f2)
echo "MongoDB $VERSION, the version src/download-mongodb.bat pins"
fetch_tgz() { # url, destination bin directory
curl -fsSL -o mongo.tgz "$1"
mkdir -p "$2"
# --strip-components drops the versioned top directory the archive
# carries, so the binary lands where the app looks for it.
# GNU tar needs --wildcards for a glob and BSD tar rejects the
# flag, so ask tar which it is rather than guessing by platform.
if tar --version 2>/dev/null | grep -qi 'gnu tar'; then
tar -xzf mongo.tgz --wildcards --strip-components=2 -C "$2" '*/bin/mongod'
else
tar -xzf mongo.tgz --strip-components=2 -C "$2" '*/bin/mongod'
fi
chmod +x "$2/mongod"
rm -f mongo.tgz
}
case "${{ matrix.name }}" in
Windows)
curl -fsSL -o mongodb.zip "https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-${VERSION}.zip"
7z x -bso0 mongodb.zip -omongodb-temp
mkdir -p mongodb/bin
cp "mongodb-temp/mongodb-win32-x86_64-windows-${VERSION}/bin/mongod.exe" mongodb/bin/
# mongod links against the Visual C++ runtime. A runner has it in
# System32; a shop's clean Windows may not, so the three DLLs
# travel with it - the same ones bundle-vc-runtime.js collects.
for dll in msvcp140.dll vcruntime140.dll vcruntime140_1.dll; do
cp "/c/Windows/System32/$dll" mongodb/bin/
done
rm -rf mongodb.zip mongodb-temp
node scripts/check-mongodb-binaries.js
;;
macOS)
fetch_tgz "https://fastdl.mongodb.org/osx/mongodb-macos-x86_64-${VERSION}.tgz" mongodb-x64/bin
fetch_tgz "https://fastdl.mongodb.org/osx/mongodb-macos-arm64-${VERSION}.tgz" mongodb-arm64/bin
ls -l mongodb-x64/bin/mongod mongodb-arm64/bin/mongod
;;
Linux)
fetch_tgz "https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-${VERSION}.tgz" mongodb/bin
ls -l mongodb/bin/mongod
;;
esac
# Everything the installer packages beyond source: the API's node_modules
# zip, the built frontend, the sync agent, the VC++ runtime. All of it is
# generated and all of it is gitignored, so a fresh checkout has none of
# it.
#
# This step did not exist. `npx electron-builder` was called directly,
# skipping the prebuild hook that produces these locally - so a release
# built an installer whose extraResources were simply absent. The apps
# this workflow produced were not the app anyone had ever tested.
- name: Prepare everything the installer bundles
run: npm run prepare:bundle
# macOS signing + notarization (RELEASE_TRUST_PLAN §3). Everything here
# is DORMANT until the Apple secrets exist: without them the build is
# exactly what it was before - unsigned, loudly labelled as such - and
# with them electron-builder signs with the Developer ID certificate,
# submits to Apple's notary service and staples the ticket. Unlike the
# Windows key (hardware card, desk only), Apple credentials are files,
# so CI is the right place for them.
- name: Stage Apple notarization credentials
if: matrix.name == 'macOS' && env.HAVE_NOTARY == 'true'
env:
ASC_API_KEY_P8: ${{ secrets.ASC_API_KEY_P8 }}
run: |
echo "$ASC_API_KEY_P8" > "$RUNNER_TEMP/asc_api_key.p8"
echo "APPLE_API_KEY=$RUNNER_TEMP/asc_api_key.p8" >> "$GITHUB_ENV"
# electron-builder would publish to the release by itself; it is turned
# off here so that a half-finished matrix never leaves a release with
# only one platform attached.
- name: Build
run: >
npx electron-builder ${{ matrix.flag }} --publish never
${{ matrix.name == 'macOS' && env.HAVE_NOTARY == 'true' && '--config.mac.notarize=true' || '' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ matrix.name == 'macOS' && secrets.MAC_CERT_P12 || '' }}
CSC_KEY_PASSWORD: ${{ matrix.name == 'macOS' && secrets.MAC_CERT_PASSWORD || '' }}
APPLE_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.ASC_API_ISSUER }}
# The gate that makes an un-notarized dmg impossible to publish once the
# credentials exist - the same promise scripts/release-windows.js makes
# for the exe: signed and accepted by the OS's own assessor, or no build.
- name: Verify macOS signature and notarization
if: matrix.name == 'macOS' && env.HAVE_MAC_CERT == 'true'
env:
APPLE_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.ASC_API_ISSUER }}
run: |
set -euo pipefail
APP=$(find dist/mac* -maxdepth 1 -name '*.app' | head -1)
codesign --verify --deep --strict --verbose=2 "$APP"
if [ "$HAVE_NOTARY" = "true" ]; then
spctl -a -vv "$APP"
# electron-builder notarizes the APP while packing - the dmg is
# born afterwards, signed but ticketless, and Gatekeeper's
# assessment of the container then says "Unnotarized Developer
# ID". The container earns its own ticket here, stapled so an
# offline install verifies too.
for DMG in dist/*.dmg; do
xcrun notarytool submit "$DMG" --key "$APPLE_API_KEY" --key-id "$APPLE_API_KEY_ID" --issuer "$APPLE_API_ISSUER" --wait
xcrun stapler staple "$DMG"
spctl -a -t open -vv --context context:primary-signature "$DMG"
done
echo "macOS artifacts are signed, notarized and Gatekeeper-approved"
else
echo "::warning::signed but NOT notarized - ASC API key not configured yet (RELEASE_TRUST_PLAN Appendix A2)"
fi
- name: Warn when the macOS build ships unsigned
if: matrix.name == 'macOS' && env.HAVE_MAC_CERT != 'true'
run: echo "::warning::macOS build is UNSIGNED - Apple secrets (MAC_CERT_P12 etc.) are not configured yet. See RELEASE_TRUST_PLAN.md §3."
# The clean-Windows runtime check moved to scripts/release-windows.js
# along with the Windows build itself. Left here it would have been a step
# that never runs - `if: matrix.name == 'Windows'` against a matrix with
# no Windows in it - which reads as coverage and is not.
# One CycloneDX document per downloadable package. Each file carries the
# package SHA-256, exact source commit, locked API graph, Electron runtime
# and the separately licensed MongoDB server bundled with the package.
- name: Keep macOS updater metadata valid after notarization
if: matrix.name == 'macOS'
run: node scripts/prepare-mac-updater.js
- name: Generate artifact SBOMs
run: npm run release:sbom
- name: Upload installers
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.name }}
path: |
dist/*.exe
dist/*.dmg
dist/*.zip
dist/*.AppImage
dist/*.deb
# Built on every Linux run (package.json declares the snap target)
# and then dropped here, so publish-snap.yml failed on every release
# trying to download a .snap the release never carried - a red X
# beside a store that was silently never updated.
dist/*.snap
dist/*.cdx.json
# The update manifest, without which auto-update cannot work at all:
# electron-updater fetches latest.yml - latest-mac.yml, latest-linux.yml
# on the others - to learn what the newest version is. It was built on
# every run and then thrown away, so every update check 404'd.
dist/latest*.yml
# Blockmaps let an update fetch only the changed parts of an installer
# rather than the whole 159 MB.
dist/*.blockmap
if-no-files-found: error
retention-days: 7
publish:
name: Publish release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
artifact-metadata: write
env:
ASSET_SIGNING_KEY: ${{ secrets.POSNIC_ASSET_SIGNING_KEY }}
steps:
- uses: actions/checkout@v7
with: { ref: '${{ inputs.tag || github.ref }}' }
- uses: actions/download-artifact@v8
with: { path: artifacts }
# The silent asset-update channel (asset-updater.js on the till):
# rebuild the frontend deterministically from this tag, sign the
# manifest with the fleet key, and ship manifest + bundle under stable
# names so /releases/latest/download/ URLs never change. Skipped
# gracefully while the signing secret is not configured - installers
# ship regardless, and tills simply keep updating by installer only.
- name: Build and sign the asset bundle
if: env.ASSET_SIGNING_KEY != ''
run: |
(cd frontend && npm ci --no-audit --no-fund && npx gulp build)
mkdir -p release
POSNIC_ASSET_SIGNING_KEY="$ASSET_SIGNING_KEY" \
node scripts/build-asset-bundle.js release/asset-manifest.json
(cd frontend/public && zip -qr ../../release/posnic-assets.zip . -x all.zip)
ls -lh release/
# Anyone downloading a point-of-sale binary should be able to check it
# arrived intact. One file, one line per installer.
- name: Collect and checksum
env:
POSNIC_PRERELEASE: ${{ vars.POSNIC_PRERELEASE }}
run: |
mkdir -p release
find artifacts -type f \( -name '*.exe' -o -name '*.dmg' -o -name '*.zip' \
-o -name '*.AppImage' -o -name '*.deb' -o -name '*.snap' -o -name 'latest*.yml' \
-o -name '*.blockmap' -o -name '*.cdx.json' \) -exec mv {} release/ \;
# The machine-readable statement of what this release IS (version,
# channel, schema + sync-protocol, oldest supported client). Fleet
# tooling and the gateway read this instead of parsing file names.
node scripts/release-manifest.js release/release-manifest.json
cd release
# The checksum file lists what a person downloads. latest.yml carries its
# own hashes for the updater, and a blockmap or the release manifest is an
# implementation detail, so listing them would only add noise to the thing
# the release notes tell people to check.
sha256sum $(ls | grep -vE 'latest.*\.yml$|\.blockmap$|release-manifest\.json$|asset-manifest\.json$') > SHA256SUMS.txt
echo "Publishing:"; ls -lh
# Bind every package and package SBOM named in SHA256SUMS.txt to this
# repository, workflow, ref and commit. The action uses a short-lived
# GitHub OIDC identity; no long-lived signing key is stored in secrets.
- name: Attest release artifact provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-checksums: release/SHA256SUMS.txt
# The notes serve two kinds of tag: the beta channel builds from here
# too (this workflow triggers on every v*), and a stable release must
# not greet customers with "this is an early public build". The beta
# warning is written only when the tag says beta; everything below it
# is true of both.
- name: Compose the release notes
env:
TAG: ${{ inputs.tag || github.ref_name }}
run: |
mkdir -p release
case "$TAG" in
*beta*|*alpha*|*rc*)
cat > release/NOTES.md <<'POSNIC_BETA'
## Beta - please read before installing
This is an early public build. It works, it is tested, and it is **not
finished**. Expect rough edges, and please report them.
POSNIC_BETA
;;
*)
cat > release/NOTES.md <<POSNIC_STABLE
## Posnic ${TAG}
A stable release. Every installer below is signed by a publisher your
operating system can check for itself - how, per platform, next.
POSNIC_STABLE
;;
esac
cat >> release/NOTES.md <<'POSNIC_COMMON'
### About the installers
- **Windows** is signed and timestamped. The installer names its
publisher, and no warning should appear.
- **macOS is signed and notarized** (Developer ID, checked by Apple's
notary service, ticket stapled) - Gatekeeper opens it like any app
from the App Store era. If you see a publisher warning, you are not
holding our build.
- **Linux**: install through the signed APT repository at
[packages.posnic.com](https://packages.posnic.com) and `apt` verifies
every package and every update from then on. The direct `.deb` and
AppImage remain, with checksums and detached GPG signatures beside them.
Whatever you download, `SHA256SUMS.txt` on this page lists what the
bytes should be - and the release is provenance-attested, so GitHub can
prove it was built by this repository's own workflow.
To check that for yourself, with the GitHub CLI installed:
`gh attestation verify <the-file-you-downloaded> --repo Posnic/POS`
That confirms the repository, workflow, ref and commit recorded for the
file. It is not a code signature, an independent audit, or proof that
the software is safe. It answers one question: did this file come out
of our build.
Automatic updates check both the hash they were promised and, on
Windows and macOS, the code signature of what actually arrived -
an update that is not ours does not install.
### What to expect
- Your data stays on your own computer. No account, and no internet needed.
- **Take your own backups.** *Backup Manager -> Backup Now*, and keep a copy on
another machine or a USB drive. This software holds your sales.
- Printing, scales and cash drawers are tested, but not on every model. Test
your hardware from *Config -> Device Setup* before a trading day.
- First launch takes a few minutes while the database is prepared. Later
launches take seconds.
### If something goes wrong
Open an issue with what you did, what happened, and your log file attached -
`%APPDATA%\posnic\app.log` on Windows. For an urgent problem where a shop
cannot trade: **+91 94941 11161**, answered any hour.
Free and open source under AGPL-3.0-only. Posnic Cloud (sync, off-site backups,
remote dashboard) is a separate paid service, and nothing here needs it.
POSNIC_COMMON
echo "--- release notes ---"; cat release/NOTES.md
- uses: softprops/action-gh-release@v3
with:
tag_name: ${{ inputs.tag || github.ref_name }}
files: release/*
generate_release_notes: true
draft: true # a human decides when it goes public
# A pre-release by default, until the installers are signed: with
# allowPrerelease false, electron-updater ignores pre-releases, so no
# existing installation is pushed a beta it did not ask for.
#
# That is also why an update cannot be tested against one. Set the
# repository variable POSNIC_PRERELEASE to "no" for a release that
# should be offered to installed copies - which is what verifying the
# update path requires.
prerelease: ${{ vars.POSNIC_PRERELEASE != 'no' }}
body_path: release/NOTES.md