@gjsify/cli 0.64.12-next.280 → 0.64.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.gjs.mjs +52 -52
- package/lib/commands/ship.js +35 -2
- package/lib/types/config-data.d.ts +2 -2
- package/lib/utils/ship/depends.js +0 -6
- package/lib/utils/ship/dmg.d.ts +50 -2
- package/lib/utils/ship/dmg.js +72 -8
- package/lib/utils/ship/release-record.d.ts +47 -0
- package/lib/utils/ship/release-record.js +55 -0
- package/lib/utils/ship/signing.d.ts +10 -9
- package/lib/utils/ship/signing.js +10 -9
- package/package.json +21 -22
package/lib/commands/ship.js
CHANGED
|
@@ -39,7 +39,7 @@ import { resolveNodeModulesClosure, stageNodeModules } from '../utils/ship/node-
|
|
|
39
39
|
import { acquireGtkRuntime } from '../utils/ship/gtk-runtime-acquire.js';
|
|
40
40
|
import { discoverPayload } from '../utils/ship/discover.js';
|
|
41
41
|
import { appDirFor, appDirPayload, appImageHostRequirements, appImageRuntimeNotice, buildAppImage, findPinnedRuntime, stampAppDirTimes, } from '../utils/ship/appimage.js';
|
|
42
|
-
import { buildDmgImage, dmgVolumeDir, dmgVolumeName } from '../utils/ship/dmg.js';
|
|
42
|
+
import { addApplicationsLink, buildDmgImage, dmgVolumeDir, dmgVolumeName, signDmgImage } from '../utils/ship/dmg.js';
|
|
43
43
|
import { assertNoDroppedFlatpakModules } from '../utils/ship/flatpak-config.js';
|
|
44
44
|
import { buildFlatpakBundle } from '../utils/ship/flatpak.js';
|
|
45
45
|
import { iconSizes, resolveAppIcon } from '../utils/ship/icons.js';
|
|
@@ -55,7 +55,8 @@ import { assertEntryRunsUnder } from '../utils/ship/entry-interpreter.js';
|
|
|
55
55
|
import { scanGiRequirements } from '../utils/ship/gi-namespaces.js';
|
|
56
56
|
import { assertLauncherMatchesInterpreter, assertPayloadMatchesArch, buildTimestamp, isArchIndependent, linuxInstallDependent, readPayloadFacts, } from '../utils/ship/payload.js';
|
|
57
57
|
import { assertOverlayIsLicensed, planOverlay, planStage } from '../utils/ship/plan.js';
|
|
58
|
-
import {
|
|
58
|
+
import { recordedSignature, writeReleaseRecord } from '../utils/ship/release-record.js';
|
|
59
|
+
import { ADHOC_IDENTITY, assertHostCanSign, notarizeArtifact, stapleArtifact, resolveNotaryPlan, resolveSignPlan, signPayload, } from '../utils/ship/signing.js';
|
|
59
60
|
import { renderLauncher } from '../utils/ship/launcher.js';
|
|
60
61
|
import { buildRpm } from '../utils/ship/rpm.js';
|
|
61
62
|
import { resolveShipApp, resolveShipBundle, resolveShipKind, resolveShipSettings, } from '../utils/ship/settings.js';
|
|
@@ -1261,6 +1262,7 @@ async function packOne(input) {
|
|
|
1261
1262
|
// mounts showing `Contents/` — a window with no application in it.
|
|
1262
1263
|
const volumeDir = dmgVolumeDir(outRoot);
|
|
1263
1264
|
writePayload(volumeDir, payload, '');
|
|
1265
|
+
addApplicationsLink(volumeDir);
|
|
1264
1266
|
await buildDmgImage({
|
|
1265
1267
|
volumeName: dmgVolumeName(settings),
|
|
1266
1268
|
sourceDir: volumeDir,
|
|
@@ -1268,6 +1270,17 @@ async function packOne(input) {
|
|
|
1268
1270
|
workDir: dirname(volumeDir),
|
|
1269
1271
|
verbose: input.verbose,
|
|
1270
1272
|
});
|
|
1273
|
+
// The IMAGE's own signature, before the notarisation below submits
|
|
1274
|
+
// it and the staple writes into it — see `utils/ship/dmg.ts`.
|
|
1275
|
+
if (input.sign.kind === 'sign') {
|
|
1276
|
+
await signDmgImage({
|
|
1277
|
+
identity: input.sign.identity,
|
|
1278
|
+
file: target,
|
|
1279
|
+
adhoc: input.sign.identity === ADHOC_IDENTITY,
|
|
1280
|
+
workDir: dirname(volumeDir),
|
|
1281
|
+
verbose: input.verbose,
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1271
1284
|
break;
|
|
1272
1285
|
}
|
|
1273
1286
|
case 'macos-app-zip':
|
|
@@ -1339,6 +1352,8 @@ async function packOne(input) {
|
|
|
1339
1352
|
// installer, so a `<App>.app` has no submittable form of its own; on darwin
|
|
1340
1353
|
// the zip beside it is the one that goes. Refusing here would make
|
|
1341
1354
|
// `--notarize` unusable with the default format set, which is both rows.
|
|
1355
|
+
let notarized = false;
|
|
1356
|
+
let stapled = false;
|
|
1342
1357
|
if (input.notarize.kind === 'notarize') {
|
|
1343
1358
|
if (format.artifactKind === 'directory') {
|
|
1344
1359
|
console.warn(`${LOG} ${format.id} is a directory, and notarytool submits an archive or an installer — ` +
|
|
@@ -1351,6 +1366,7 @@ async function packOne(input) {
|
|
|
1351
1366
|
verbose: input.verbose,
|
|
1352
1367
|
log: (line) => console.log(`${LOG} ${line}`),
|
|
1353
1368
|
});
|
|
1369
|
+
notarized = true;
|
|
1354
1370
|
// AND THEN THE TICKET, on the artifacts that can hold one. Submitting
|
|
1355
1371
|
// and stapling are two questions with two answers, and the `.zip` is
|
|
1356
1372
|
// where they come apart: the notary service accepts it and `stapler`
|
|
@@ -1365,6 +1381,7 @@ async function packOne(input) {
|
|
|
1365
1381
|
verbose: input.verbose,
|
|
1366
1382
|
log: (line) => console.log(`${LOG} ${line}`),
|
|
1367
1383
|
});
|
|
1384
|
+
stapled = true;
|
|
1368
1385
|
}
|
|
1369
1386
|
else {
|
|
1370
1387
|
console.warn(`${LOG} ${format.id} is notarised and NOT stapled: \`stapler\` takes a UDIF image, a ` +
|
|
@@ -1374,6 +1391,22 @@ async function packOne(input) {
|
|
|
1374
1391
|
}
|
|
1375
1392
|
}
|
|
1376
1393
|
}
|
|
1394
|
+
// THE RELEASE RECORD, after every step that writes into the file (ADR 0124).
|
|
1395
|
+
// A directory is never a release asset; the container beside it is.
|
|
1396
|
+
if (format.artifactKind === 'file') {
|
|
1397
|
+
const record = writeReleaseRecord({
|
|
1398
|
+
file: target,
|
|
1399
|
+
format: format.id,
|
|
1400
|
+
os: format.layoutOs,
|
|
1401
|
+
arch: isArchIndependent(payload) ? 'noarch' : input.arch,
|
|
1402
|
+
signature: recordedSignature(input.sign.kind === 'sign' ? input.sign.identity : undefined, ADHOC_IDENTITY),
|
|
1403
|
+
notarized,
|
|
1404
|
+
stapled,
|
|
1405
|
+
generator: `@gjsify/cli ${cliVersion()}`,
|
|
1406
|
+
});
|
|
1407
|
+
if (input.verbose)
|
|
1408
|
+
console.log(`${LOG} release record: ${record}`);
|
|
1409
|
+
}
|
|
1377
1410
|
return { format: format.id, path: target, size };
|
|
1378
1411
|
}
|
|
1379
1412
|
function printArtifacts(base, artifacts) {
|
|
@@ -635,8 +635,8 @@ export interface ConfigDataShip extends AppMetadata {
|
|
|
635
635
|
* Additions for ONE package format — `deb` or `rpm` — on top of everything above.
|
|
636
636
|
*
|
|
637
637
|
* Exists for a library the distros disagree about: one that Fedora ships and Ubuntu does
|
|
638
|
-
* not
|
|
639
|
-
*
|
|
638
|
+
* not, where the `.rpm` should use the system copy and only the `.deb` needs to carry
|
|
639
|
+
* its own. `bundledTypelibs` and `extraFiles` are staged for every format of a
|
|
640
640
|
* layout, so there is no way to say "the `.deb` only" with them.
|
|
641
641
|
*
|
|
642
642
|
* What a format carries here is read back off its package like any other staged file, so a
|
|
@@ -232,12 +232,6 @@ const TYPELIB_PACKAGES = {
|
|
|
232
232
|
'Notify-0.7': { deb: 'gir1.2-notify-0.7', rpm: 'libnotify' },
|
|
233
233
|
'Vte-3.91': { deb: 'gir1.2-vte-3.91', rpm: 'vte291-gtk4' },
|
|
234
234
|
'Manette-0.2': { deb: 'gir1.2-manette-0.2', rpm: 'libmanette' },
|
|
235
|
-
// libgda — what `@gjsify/sqlite` binds, so every project with a local database lands here.
|
|
236
|
-
// The rpm name is measured on this machine (`rpm -qf …/Gda-6.0.typelib` → libgda); the deb name
|
|
237
|
-
// follows the `gir1.2-<name>-<version>` convention every row above uses and is NOT verified
|
|
238
|
-
// against a Debian system. Said plainly because a wrong name fails at `apt install` with a
|
|
239
|
-
// clear error, while no row at all fails the BUILD of every such project.
|
|
240
|
-
'Gda-6.0': { deb: 'gir1.2-gda-6.0', rpm: 'libgda' },
|
|
241
235
|
};
|
|
242
236
|
/**
|
|
243
237
|
* A typelib the payload CARRIES, rather than one the system provides.
|
package/lib/utils/ship/dmg.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export declare function dmgVolumeName(settings: PackSettings): string;
|
|
|
22
22
|
export interface DmgCreateInput {
|
|
23
23
|
/** The volume label — see {@link dmgVolumeName}. */
|
|
24
24
|
volumeName: string;
|
|
25
|
-
/** The directory whose CONTENTS become the volume root
|
|
25
|
+
/** The directory whose CONTENTS become the volume root: `<App>.app` and the `Applications` link. */
|
|
26
26
|
sourceDir: string;
|
|
27
27
|
/** Absolute path the finished `.dmg` is written to. */
|
|
28
28
|
target: string;
|
|
@@ -63,9 +63,57 @@ export declare function buildDmgImage(input: DmgPackInput): Promise<void>;
|
|
|
63
63
|
* Where the volume is assembled before `hdiutil` turns it into an image.
|
|
64
64
|
*
|
|
65
65
|
* A directory of its own under the run's output root, never `out/` itself: the
|
|
66
|
-
* volume root holds the `.app` and NOTHING ELSE, because `-srcfolder` copies
|
|
66
|
+
* volume root holds the `.app` and its drop target and NOTHING ELSE, because `-srcfolder` copies
|
|
67
67
|
* whatever it finds — a stray sibling would ride into the image, and the reader
|
|
68
68
|
* that would catch it is the same listing comparison that would then have to be
|
|
69
69
|
* relaxed to allow it.
|
|
70
70
|
*/
|
|
71
71
|
export declare function dmgVolumeDir(outRoot: string): string;
|
|
72
|
+
/** The drop target's name in the volume root, and where it points. */
|
|
73
|
+
export declare const DMG_APPLICATIONS_LINK: {
|
|
74
|
+
readonly name: "Applications";
|
|
75
|
+
readonly target: "/Applications";
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Put the drag-here link into a volume `writePayload` has just filled.
|
|
79
|
+
*
|
|
80
|
+
* AFTER `writePayload`, never before: that call wipes the directory it writes,
|
|
81
|
+
* so it is also what keeps a previous run's link from making this one fail
|
|
82
|
+
* with `EEXIST`. An ABSOLUTE target, because the link is resolved on the
|
|
83
|
+
* user's machine where `/Applications` is the only path that means anything —
|
|
84
|
+
* the Finder shows it as "Applications" with the folder's own icon.
|
|
85
|
+
*/
|
|
86
|
+
export declare function addApplicationsLink(volumeDir: string): void;
|
|
87
|
+
/** The tool that signs the image. The bundle's signer uses the same one. */
|
|
88
|
+
export declare const DMG_SIGN_TOOL = "codesign";
|
|
89
|
+
export interface DmgSignInput {
|
|
90
|
+
/** A `codesign` identity, or `-` for ad-hoc. */
|
|
91
|
+
identity: string;
|
|
92
|
+
/** The finished `.dmg`. */
|
|
93
|
+
file: string;
|
|
94
|
+
/** `identity` is the ad-hoc `-`. */
|
|
95
|
+
adhoc: boolean;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The `codesign` invocation for the IMAGE, as data.
|
|
99
|
+
*
|
|
100
|
+
* NOT the darwin signer's argv (`SIGNERS.darwin.args`), and the two differences
|
|
101
|
+
* are the point. No `--options runtime`: the hardened runtime is a property of
|
|
102
|
+
* code that executes, and a disk image does not. No entitlements, for the same
|
|
103
|
+
* reason. What stays is `--force` — `-ov` rewrites the file, but a caller may
|
|
104
|
+
* hand in one that is already signed — and `--timestamp` on a real identity
|
|
105
|
+
* only, for the reason that row gives: an ad-hoc signature has no certificate
|
|
106
|
+
* to countersign, and `codesign(1)` documents a timestamp failure as fatal.
|
|
107
|
+
*/
|
|
108
|
+
export declare function codesignDmgArgs(input: DmgSignInput): string[];
|
|
109
|
+
/**
|
|
110
|
+
* Sign the image at `input.file`.
|
|
111
|
+
*
|
|
112
|
+
* Ad-hoc is signed too. It earns the image nothing with Gatekeeper, but it is
|
|
113
|
+
* what CI runs, and a branch only a release with a real certificate reaches is
|
|
114
|
+
* a branch that breaks on release day.
|
|
115
|
+
*/
|
|
116
|
+
export declare function signDmgImage(input: DmgSignInput & {
|
|
117
|
+
verbose: boolean;
|
|
118
|
+
workDir: string;
|
|
119
|
+
}): Promise<void>;
|
package/lib/utils/ship/dmg.js
CHANGED
|
@@ -39,13 +39,25 @@
|
|
|
39
39
|
// ubuntu-24.04 ships 7-Zip 23.01, not this workstation's 26.02. zlib is the
|
|
40
40
|
// method its `Dmg` handler has read since long before either.
|
|
41
41
|
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// a
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
42
|
+
// THE VOLUME CARRIES ONE SYMLINK, `Applications → /Applications`, the drag-here
|
|
43
|
+
// convention: without it the mounted window shows the bundle and nothing to drop
|
|
44
|
+
// it on. It is NOT staged. `writePayload` writes regular files and nothing else —
|
|
45
|
+
// a property `tests/e2e/ship` and the `ship-stage` job both assert — and a staged
|
|
46
|
+
// symlink would be dereferenced by `actions/upload-artifact` on its way to the
|
|
47
|
+
// packing host. The volume directory is assembled ON that host, after the stage
|
|
48
|
+
// has arrived, so neither objection reaches it: the link is written here, next to
|
|
49
|
+
// the `hdiutil` call that consumes it. Its reader is `verify-dmg.py`, which admits
|
|
50
|
+
// exactly this one link with exactly this target and refuses any other.
|
|
51
|
+
//
|
|
52
|
+
// THE IMAGE ITSELF IS SIGNED, and that is a separate signature from the bundle's.
|
|
53
|
+
// Measured on macOS 27 with a Developer ID identity (2026-10-29): a notarised and
|
|
54
|
+
// stapled `.dmg` whose container was never passed to `codesign` reads "code object
|
|
55
|
+
// is not signed at all" under `codesign -dv`, and `spctl -a -t open
|
|
56
|
+
// --context context:primary-signature` rejects it with "no usable signature" —
|
|
57
|
+
// while the `.app` mounted from it is "accepted, source=Notarized Developer ID".
|
|
58
|
+
// The order is fixed by `stapler(1)`: a later `codesign` invalidates a stapled
|
|
59
|
+
// ticket, so the image is signed here, before `ship` notarises and staples it.
|
|
60
|
+
import { symlinkSync } from 'node:fs';
|
|
49
61
|
import { join } from 'node:path';
|
|
50
62
|
import { describeExit, spawnToCompletion } from '../spawn.js';
|
|
51
63
|
/** The tool this packer execs. One spelling, read by this module AND by the format row. */
|
|
@@ -171,7 +183,7 @@ export async function buildDmgImage(input) {
|
|
|
171
183
|
* Where the volume is assembled before `hdiutil` turns it into an image.
|
|
172
184
|
*
|
|
173
185
|
* A directory of its own under the run's output root, never `out/` itself: the
|
|
174
|
-
* volume root holds the `.app` and NOTHING ELSE, because `-srcfolder` copies
|
|
186
|
+
* volume root holds the `.app` and its drop target and NOTHING ELSE, because `-srcfolder` copies
|
|
175
187
|
* whatever it finds — a stray sibling would ride into the image, and the reader
|
|
176
188
|
* that would catch it is the same listing comparison that would then have to be
|
|
177
189
|
* relaxed to allow it.
|
|
@@ -179,3 +191,55 @@ export async function buildDmgImage(input) {
|
|
|
179
191
|
export function dmgVolumeDir(outRoot) {
|
|
180
192
|
return join(outRoot, 'dmg', 'volume');
|
|
181
193
|
}
|
|
194
|
+
/** The drop target's name in the volume root, and where it points. */
|
|
195
|
+
export const DMG_APPLICATIONS_LINK = { name: 'Applications', target: '/Applications' };
|
|
196
|
+
/**
|
|
197
|
+
* Put the drag-here link into a volume `writePayload` has just filled.
|
|
198
|
+
*
|
|
199
|
+
* AFTER `writePayload`, never before: that call wipes the directory it writes,
|
|
200
|
+
* so it is also what keeps a previous run's link from making this one fail
|
|
201
|
+
* with `EEXIST`. An ABSOLUTE target, because the link is resolved on the
|
|
202
|
+
* user's machine where `/Applications` is the only path that means anything —
|
|
203
|
+
* the Finder shows it as "Applications" with the folder's own icon.
|
|
204
|
+
*/
|
|
205
|
+
export function addApplicationsLink(volumeDir) {
|
|
206
|
+
symlinkSync(DMG_APPLICATIONS_LINK.target, join(volumeDir, DMG_APPLICATIONS_LINK.name));
|
|
207
|
+
}
|
|
208
|
+
/** The tool that signs the image. The bundle's signer uses the same one. */
|
|
209
|
+
export const DMG_SIGN_TOOL = 'codesign';
|
|
210
|
+
/**
|
|
211
|
+
* The `codesign` invocation for the IMAGE, as data.
|
|
212
|
+
*
|
|
213
|
+
* NOT the darwin signer's argv (`SIGNERS.darwin.args`), and the two differences
|
|
214
|
+
* are the point. No `--options runtime`: the hardened runtime is a property of
|
|
215
|
+
* code that executes, and a disk image does not. No entitlements, for the same
|
|
216
|
+
* reason. What stays is `--force` — `-ov` rewrites the file, but a caller may
|
|
217
|
+
* hand in one that is already signed — and `--timestamp` on a real identity
|
|
218
|
+
* only, for the reason that row gives: an ad-hoc signature has no certificate
|
|
219
|
+
* to countersign, and `codesign(1)` documents a timestamp failure as fatal.
|
|
220
|
+
*/
|
|
221
|
+
export function codesignDmgArgs(input) {
|
|
222
|
+
return ['--force', '--sign', input.identity, ...(input.adhoc ? [] : ['--timestamp']), input.file];
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Sign the image at `input.file`.
|
|
226
|
+
*
|
|
227
|
+
* Ad-hoc is signed too. It earns the image nothing with Gatekeeper, but it is
|
|
228
|
+
* what CI runs, and a branch only a release with a real certificate reaches is
|
|
229
|
+
* a branch that breaks on release day.
|
|
230
|
+
*/
|
|
231
|
+
export async function signDmgImage(input) {
|
|
232
|
+
const args = codesignDmgArgs(input);
|
|
233
|
+
if (input.verbose)
|
|
234
|
+
console.log(`[gjsify ship] ${DMG_SIGN_TOOL} ${args.join(' ')}`);
|
|
235
|
+
const result = await spawnToCompletion(DMG_SIGN_TOOL, args, {
|
|
236
|
+
completion: 'return',
|
|
237
|
+
cwd: input.workDir,
|
|
238
|
+
stdio: 'inherit',
|
|
239
|
+
notFound: () => new Error(`gjsify ship: ${DMG_SIGN_TOOL} is not on PATH, so the image cannot be signed. Install the ` +
|
|
240
|
+
'Xcode Command Line Tools (`xcode-select --install`).'),
|
|
241
|
+
});
|
|
242
|
+
if (result.code !== 0) {
|
|
243
|
+
throw new Error(`gjsify ship: ${DMG_SIGN_TOOL} failed on the image with ${describeExit(result)}.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { FormatId, HostOs } from './types.js';
|
|
2
|
+
/** The suffix a record's name adds to its artifact's. The site matches on it. */
|
|
3
|
+
export declare const RELEASE_RECORD_SUFFIX = ".gjsify-release.json";
|
|
4
|
+
export declare const RELEASE_RECORD_SCHEMA = 1;
|
|
5
|
+
export type RecordedSignature = {
|
|
6
|
+
kind: 'none';
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'adhoc';
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'identity';
|
|
11
|
+
identity: string;
|
|
12
|
+
};
|
|
13
|
+
export interface ReleaseRecord {
|
|
14
|
+
schema: typeof RELEASE_RECORD_SCHEMA;
|
|
15
|
+
/** The artifact's file name, which is also its release asset name. */
|
|
16
|
+
artifact: string;
|
|
17
|
+
/** Lower-case hex SHA-256 of the artifact as written. */
|
|
18
|
+
sha256: string;
|
|
19
|
+
size: number;
|
|
20
|
+
format: FormatId;
|
|
21
|
+
os: HostOs;
|
|
22
|
+
/** `process.arch` spelling, or `noarch` for a payload with no native code. */
|
|
23
|
+
arch: string;
|
|
24
|
+
signature: RecordedSignature;
|
|
25
|
+
notarized: boolean;
|
|
26
|
+
stapled: boolean;
|
|
27
|
+
/** `@gjsify/cli <version>` — who wrote this record. */
|
|
28
|
+
generator: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ReleaseRecordInput {
|
|
31
|
+
/** Absolute path of the finished artifact file. */
|
|
32
|
+
file: string;
|
|
33
|
+
format: FormatId;
|
|
34
|
+
os: HostOs;
|
|
35
|
+
arch: string;
|
|
36
|
+
signature: RecordedSignature;
|
|
37
|
+
notarized: boolean;
|
|
38
|
+
stapled: boolean;
|
|
39
|
+
generator: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function releaseRecordPath(artifact: string): string;
|
|
42
|
+
/** Map a run's signing identity to what the record says; `-` is ad-hoc. */
|
|
43
|
+
export declare function recordedSignature(identity: string | undefined, adhocIdentity: string): RecordedSignature;
|
|
44
|
+
/** Build the record from the bytes at `input.file`. Pure apart from reading that file. */
|
|
45
|
+
export declare function buildReleaseRecord(input: ReleaseRecordInput): ReleaseRecord;
|
|
46
|
+
/** Write the record beside its artifact and return the record's path. */
|
|
47
|
+
export declare function writeReleaseRecord(input: ReleaseRecordInput): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// The release record — ADR 0124.
|
|
2
|
+
//
|
|
3
|
+
// ONE FILE BESIDE EVERY FILE ARTIFACT, `<artifact>.gjsify-release.json`, saying
|
|
4
|
+
// what this run did to it: which signature it carries, whether Apple accepted
|
|
5
|
+
// it, whether a ticket was stapled. `@gjsify/site` reads it to decide whether a
|
|
6
|
+
// download needs the "how to open an unsigned app" hint.
|
|
7
|
+
//
|
|
8
|
+
// WRITTEN LAST, from outcomes and not from flags. The hash is taken from the
|
|
9
|
+
// bytes on disk after `stapler` wrote into them, because a hash from before
|
|
10
|
+
// the staple describes a file nobody downloads. A step that fails throws
|
|
11
|
+
// before this runs, so a record never claims a step that did not finish.
|
|
12
|
+
//
|
|
13
|
+
// Per artifact rather than per run or per release: a release is assembled by
|
|
14
|
+
// several jobs on several hosts, and a file named by its artifact cannot
|
|
15
|
+
// collide with another job's.
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { basename } from 'node:path';
|
|
19
|
+
/** The suffix a record's name adds to its artifact's. The site matches on it. */
|
|
20
|
+
export const RELEASE_RECORD_SUFFIX = '.gjsify-release.json';
|
|
21
|
+
export const RELEASE_RECORD_SCHEMA = 1;
|
|
22
|
+
export function releaseRecordPath(artifact) {
|
|
23
|
+
return `${artifact}${RELEASE_RECORD_SUFFIX}`;
|
|
24
|
+
}
|
|
25
|
+
/** Map a run's signing identity to what the record says; `-` is ad-hoc. */
|
|
26
|
+
export function recordedSignature(identity, adhocIdentity) {
|
|
27
|
+
if (identity === undefined)
|
|
28
|
+
return { kind: 'none' };
|
|
29
|
+
if (identity === adhocIdentity)
|
|
30
|
+
return { kind: 'adhoc' };
|
|
31
|
+
return { kind: 'identity', identity };
|
|
32
|
+
}
|
|
33
|
+
/** Build the record from the bytes at `input.file`. Pure apart from reading that file. */
|
|
34
|
+
export function buildReleaseRecord(input) {
|
|
35
|
+
const bytes = readFileSync(input.file);
|
|
36
|
+
return {
|
|
37
|
+
schema: RELEASE_RECORD_SCHEMA,
|
|
38
|
+
artifact: basename(input.file),
|
|
39
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
40
|
+
size: bytes.length,
|
|
41
|
+
format: input.format,
|
|
42
|
+
os: input.os,
|
|
43
|
+
arch: input.arch,
|
|
44
|
+
signature: input.signature,
|
|
45
|
+
notarized: input.notarized,
|
|
46
|
+
stapled: input.stapled,
|
|
47
|
+
generator: input.generator,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Write the record beside its artifact and return the record's path. */
|
|
51
|
+
export function writeReleaseRecord(input) {
|
|
52
|
+
const path = releaseRecordPath(input.file);
|
|
53
|
+
writeFileSync(path, `${JSON.stringify(buildReleaseRecord(input), null, 4)}\n`);
|
|
54
|
+
return path;
|
|
55
|
+
}
|
|
@@ -266,12 +266,12 @@ export declare function resolveNotaryPlan(input: {
|
|
|
266
266
|
/**
|
|
267
267
|
* Submit one written artifact for notarisation.
|
|
268
268
|
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
269
|
+
* VERIFIED BY HAND, NOT IN CI. The argv is covered by `signing.spec.ts`, the
|
|
270
|
+
* guard and both refusals by `tests/e2e/ship-signing`; the invocation needs an
|
|
271
|
+
* Apple Developer account, which § A17 keeps out of CI. It has been run with a
|
|
272
|
+
* real Developer ID and keychain profile on 2026-10-29 (ADR 0024, amendment of
|
|
273
|
+
* that date): the `.dmg` and the `.zip` of a GTK application both came back
|
|
274
|
+
* `Accepted`.
|
|
275
275
|
*/
|
|
276
276
|
export declare function notarizeArtifact(input: {
|
|
277
277
|
plan: Extract<NotaryPlan, {
|
|
@@ -298,9 +298,10 @@ export declare function notarizeArtifact(input: {
|
|
|
298
298
|
* instead — which is a fact about the CONTAINER and therefore lives on the format
|
|
299
299
|
* (`canCarryTicket`), not in a branch here.
|
|
300
300
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
301
|
+
* VERIFIED BY HAND, for the same reason as `notarizeArtifact`: stapling needs a
|
|
302
|
+
* real notarisation (ADR 0024 § A15, § A17). Run on 2026-10-29 against a signed
|
|
303
|
+
* `.dmg` — "The staple and validate action worked!", and `spctl` accepts the
|
|
304
|
+
* image afterwards. The argv is unit-tested.
|
|
304
305
|
*
|
|
305
306
|
* `stapler(1)` also states the constraint that decides the ORDER: *"Stapling does
|
|
306
307
|
* not invalidate the code signature and must be run after an executable or archive
|
|
@@ -461,12 +461,12 @@ export function resolveNotaryPlan(input) {
|
|
|
461
461
|
/**
|
|
462
462
|
* Submit one written artifact for notarisation.
|
|
463
463
|
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
464
|
+
* VERIFIED BY HAND, NOT IN CI. The argv is covered by `signing.spec.ts`, the
|
|
465
|
+
* guard and both refusals by `tests/e2e/ship-signing`; the invocation needs an
|
|
466
|
+
* Apple Developer account, which § A17 keeps out of CI. It has been run with a
|
|
467
|
+
* real Developer ID and keychain profile on 2026-10-29 (ADR 0024, amendment of
|
|
468
|
+
* that date): the `.dmg` and the `.zip` of a GTK application both came back
|
|
469
|
+
* `Accepted`.
|
|
470
470
|
*/
|
|
471
471
|
export async function notarizeArtifact(input) {
|
|
472
472
|
const { notary, credential } = input.plan;
|
|
@@ -504,9 +504,10 @@ export async function notarizeArtifact(input) {
|
|
|
504
504
|
* instead — which is a fact about the CONTAINER and therefore lives on the format
|
|
505
505
|
* (`canCarryTicket`), not in a branch here.
|
|
506
506
|
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
507
|
+
* VERIFIED BY HAND, for the same reason as `notarizeArtifact`: stapling needs a
|
|
508
|
+
* real notarisation (ADR 0024 § A15, § A17). Run on 2026-10-29 against a signed
|
|
509
|
+
* `.dmg` — "The staple and validate action worked!", and `spctl` accepts the
|
|
510
|
+
* image afterwards. The argv is unit-tested.
|
|
510
511
|
*
|
|
511
512
|
* `stapler(1)` also states the constraint that decides the ORDER: *"Stapling does
|
|
512
513
|
* not invalidate the code signature and must be run after an executable or archive
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.64.12
|
|
3
|
+
"version": "0.64.12",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -142,22 +142,22 @@
|
|
|
142
142
|
"cli"
|
|
143
143
|
],
|
|
144
144
|
"dependencies": {
|
|
145
|
-
"@gjsify/blueprint": "0.64.12
|
|
146
|
-
"@gjsify/buffer": "0.64.12
|
|
147
|
-
"@gjsify/create-app": "0.64.12
|
|
148
|
-
"@gjsify/node-globals": "0.64.12
|
|
149
|
-
"@gjsify/node-polyfills": "0.64.12
|
|
150
|
-
"@gjsify/npm-registry": "0.64.12
|
|
151
|
-
"@gjsify/resolve-npm": "0.64.12
|
|
152
|
-
"@gjsify/rolldown-plugin-gjsify": "0.64.12
|
|
153
|
-
"@gjsify/vite-plugin-blueprint": "0.64.12
|
|
154
|
-
"@gjsify/rolldown-plugin-pnp": "0.64.12
|
|
155
|
-
"@gjsify/semver": "0.64.12
|
|
156
|
-
"@gjsify/tar": "0.64.12
|
|
157
|
-
"@gjsify/tsc": "0.64.12
|
|
158
|
-
"@gjsify/utils": "0.64.12
|
|
159
|
-
"@gjsify/web-polyfills": "0.64.12
|
|
160
|
-
"@gjsify/workspace": "0.64.12
|
|
145
|
+
"@gjsify/blueprint": "^0.64.12",
|
|
146
|
+
"@gjsify/buffer": "^0.64.12",
|
|
147
|
+
"@gjsify/create-app": "^0.64.12",
|
|
148
|
+
"@gjsify/node-globals": "^0.64.12",
|
|
149
|
+
"@gjsify/node-polyfills": "^0.64.12",
|
|
150
|
+
"@gjsify/npm-registry": "^0.64.12",
|
|
151
|
+
"@gjsify/resolve-npm": "^0.64.12",
|
|
152
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.64.12",
|
|
153
|
+
"@gjsify/vite-plugin-blueprint": "^0.64.12",
|
|
154
|
+
"@gjsify/rolldown-plugin-pnp": "^0.64.12",
|
|
155
|
+
"@gjsify/semver": "^0.64.12",
|
|
156
|
+
"@gjsify/tar": "^0.64.12",
|
|
157
|
+
"@gjsify/tsc": "^0.64.12",
|
|
158
|
+
"@gjsify/utils": "^0.64.12",
|
|
159
|
+
"@gjsify/web-polyfills": "^0.64.12",
|
|
160
|
+
"@gjsify/workspace": "^0.64.12",
|
|
161
161
|
"acorn": "^8.17.0",
|
|
162
162
|
"cosmiconfig": "^9.0.2",
|
|
163
163
|
"get-tsconfig": "^4.14.0",
|
|
@@ -166,13 +166,13 @@
|
|
|
166
166
|
"yargs": "^18.0.0"
|
|
167
167
|
},
|
|
168
168
|
"devDependencies": {
|
|
169
|
-
"@gjsify/unit": "0.64.12
|
|
169
|
+
"@gjsify/unit": "^0.64.12",
|
|
170
170
|
"@types/yargs": "^17.0.35",
|
|
171
171
|
"typescript": "^6.0.3"
|
|
172
172
|
},
|
|
173
173
|
"peerDependencies": {
|
|
174
|
-
"@gjsify/oxfmt-native": "0.64.12
|
|
175
|
-
"@gjsify/rolldown-native": "0.64.12
|
|
174
|
+
"@gjsify/oxfmt-native": "^0.64.12",
|
|
175
|
+
"@gjsify/rolldown-native": "^0.64.12"
|
|
176
176
|
},
|
|
177
177
|
"peerDependenciesMeta": {
|
|
178
178
|
"@gjsify/oxfmt-native": {
|
|
@@ -191,6 +191,5 @@
|
|
|
191
191
|
"bugs": {
|
|
192
192
|
"url": "https://github.com/gjsify/gjsify/issues"
|
|
193
193
|
},
|
|
194
|
-
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/cli#readme"
|
|
195
|
-
"gitHead": "0a9185d91127072fce6aa54dc126e87e6ecd0c3d"
|
|
194
|
+
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/cli#readme"
|
|
196
195
|
}
|