@coreframe/scripts 0.1.0 → 0.1.2

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.
Files changed (96) hide show
  1. package/dist/config.d.ts +18 -4
  2. package/dist/config.js +1 -3
  3. package/dist/coreframe.d.ts +243 -241
  4. package/dist/coreframe.js +1 -239
  5. package/dist/db/client.d.ts +26 -18
  6. package/dist/db/client.js +1 -29
  7. package/dist/db/config.d.ts +22 -19
  8. package/dist/db/config.js +1 -32
  9. package/dist/db/drizzle-kit.d.ts +6 -5
  10. package/dist/db/drizzle-kit.js +1 -21
  11. package/dist/db/ensure.d.ts +20 -2
  12. package/dist/db/ensure.js +1 -39
  13. package/dist/db/migrate-held.d.ts +16 -10
  14. package/dist/db/migrate-held.js +3 -112
  15. package/dist/db/migrate.d.ts +4 -2
  16. package/dist/db/migrate.js +1 -24
  17. package/dist/db/project.d.ts +19 -7
  18. package/dist/db/project.js +1 -9
  19. package/dist/db/reset.d.ts +4 -2
  20. package/dist/db/reset.js +1 -18
  21. package/dist/db/schema-push.d.ts +22 -0
  22. package/dist/db/schema-push.js +1 -0
  23. package/dist/db/seed.d.ts +6 -3
  24. package/dist/db/seed.js +1 -22
  25. package/dist/db/setup.d.ts +6 -3
  26. package/dist/db/setup.js +1 -14
  27. package/dist/deploy/check-migrations.d.ts +4 -2
  28. package/dist/deploy/check-migrations.js +1 -17
  29. package/dist/deploy/checks.d.ts +6 -2
  30. package/dist/deploy/checks.js +1 -27
  31. package/dist/deploy/config.d.ts +47 -44
  32. package/dist/deploy/config.js +1 -9
  33. package/dist/deploy/migrations.d.ts +21 -18
  34. package/dist/deploy/migrations.js +4 -203
  35. package/dist/deploy/pending-migrations.d.ts +9 -5
  36. package/dist/deploy/pending-migrations.js +1 -30
  37. package/dist/deploy/tauri-release.d.ts +93 -73
  38. package/dist/deploy/tauri-release.js +1 -512
  39. package/dist/deploy/version.d.ts +15 -13
  40. package/dist/deploy/version.js +1 -41
  41. package/dist/deploy.d.ts +16 -15
  42. package/dist/deploy.js +1 -246
  43. package/dist/dev.d.ts +23 -12
  44. package/dist/dev.js +1 -95
  45. package/dist/image-generator.d.ts +18 -2
  46. package/dist/image-generator.js +1 -59
  47. package/dist/project.d.ts +4 -1
  48. package/dist/project.js +1 -13
  49. package/dist/typecheck.d.ts +4 -2
  50. package/dist/typecheck.js +1 -44
  51. package/dist/upgrade/check-skill-current.d.ts +10 -5
  52. package/dist/upgrade/check-skill-current.js +1 -87
  53. package/dist/upgrade/ensure-template-remote.d.ts +10 -5
  54. package/dist/upgrade/ensure-template-remote.js +1 -56
  55. package/dist/upgrade/fast-forward-template-files.d.ts +8 -6
  56. package/dist/upgrade/fast-forward-template-files.js +1 -279
  57. package/package.json +7 -2
  58. package/config.test.ts +0 -15
  59. package/config.ts +0 -9
  60. package/coreframe.ts +0 -272
  61. package/db/client.ts +0 -72
  62. package/db/config.ts +0 -41
  63. package/db/drizzle-kit.test.ts +0 -27
  64. package/db/drizzle-kit.ts +0 -39
  65. package/db/ensure.ts +0 -67
  66. package/db/migrate-held.test.ts +0 -81
  67. package/db/migrate-held.ts +0 -166
  68. package/db/migrate.ts +0 -39
  69. package/db/project.ts +0 -28
  70. package/db/reset.ts +0 -24
  71. package/db/seed.ts +0 -38
  72. package/db/setup.ts +0 -18
  73. package/deploy/check-migrations.ts +0 -31
  74. package/deploy/checks.ts +0 -38
  75. package/deploy/config.test.ts +0 -24
  76. package/deploy/config.ts +0 -68
  77. package/deploy/migrations.test.ts +0 -110
  78. package/deploy/migrations.ts +0 -317
  79. package/deploy/pending-migrations.test.ts +0 -93
  80. package/deploy/pending-migrations.ts +0 -45
  81. package/deploy/tauri-release.test.ts +0 -196
  82. package/deploy/tauri-release.ts +0 -816
  83. package/deploy/version.test.ts +0 -29
  84. package/deploy/version.ts +0 -65
  85. package/deploy.test.ts +0 -101
  86. package/deploy.ts +0 -346
  87. package/dev.test.ts +0 -153
  88. package/dev.ts +0 -155
  89. package/image-generator.ts +0 -84
  90. package/project.ts +0 -6
  91. package/tsconfig.build.json +0 -12
  92. package/tsconfig.json +0 -20
  93. package/typecheck.ts +0 -55
  94. package/upgrade/check-skill-current.ts +0 -112
  95. package/upgrade/ensure-template-remote.ts +0 -79
  96. package/upgrade/fast-forward-template-files.ts +0 -408
@@ -1,816 +0,0 @@
1
- import { randomUUID } from "node:crypto"
2
- import { constants } from "node:fs"
3
- import { access, cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"
4
- import { basename, dirname, join, relative, resolve, sep } from "node:path"
5
- import { setTimeout as delay } from "node:timers/promises"
6
-
7
- import nanoSpawn from "nano-spawn"
8
- import { glob } from "tinyglobby"
9
-
10
- import type { TauriReleaseConfig, TauriReleasePlatform } from "./config.ts"
11
- import { tauriReleasePlatforms } from "./config.ts"
12
-
13
- export type TauriReleaseArgs = {
14
- tauriLocalPlatforms: TauriReleasePlatform[]
15
- tauriWorkflowPlatforms: TauriReleasePlatform[]
16
- tauriWorkflowName?: string
17
- tauriWorkflowRef?: string
18
- }
19
-
20
- export type TauriReleasePlan = {
21
- artifactDownloadDir: string
22
- b2ApplicationKey?: string
23
- b2ApplicationKeyId?: string
24
- b2Bucket?: string
25
- enabled: boolean
26
- latestManifestPath: string
27
- localPlatforms: TauriReleasePlatform[]
28
- platform: Record<TauriReleasePlatform, ResolvedTauriReleasePlatformConfig>
29
- publicBaseUrl?: string
30
- publicPathPrefix: string
31
- releaseNotesPath?: string
32
- releasePath: string
33
- releaseUploadDir: string
34
- signingPrivateKey?: string
35
- workflowName: string
36
- workflowPlatforms: TauriReleasePlatform[]
37
- workflowPollIntervalMs: number
38
- workflowRef?: string
39
- workflowTimeoutMs: number
40
- }
41
-
42
- export type TauriReleaseMetadataSnapshot = {
43
- files: { path: string; source: string }[]
44
- }
45
-
46
- export type PreparedTauriRelease = {
47
- manifestPath: string
48
- uploadDir: string
49
- }
50
-
51
- type ResolvedTauriReleasePlatformConfig = {
52
- artifactGlobs: string[]
53
- buildArgs: string[]
54
- targetTriple: string
55
- updatePlatform: string
56
- }
57
-
58
- type TauriUpdateManifest = {
59
- notes?: string
60
- platforms: Record<string, { signature: string; url: string }>
61
- pub_date: string
62
- version: string
63
- }
64
-
65
- const defaultWorkflowName = "tauri-release-artifacts.yml"
66
- const defaultWorkflowPollIntervalMs = 15_000
67
- const defaultWorkflowTimeoutMs = 60 * 60_000
68
-
69
- const defaultPlatformConfig: Record<TauriReleasePlatform, ResolvedTauriReleasePlatformConfig> = {
70
- android: {
71
- artifactGlobs: ["tauri/src-tauri/target/**/*.sig", "tauri/gen/android/**/*.sig"],
72
- buildArgs: ["tauri", "android", "build", "--target", "aarch64-linux-android"],
73
- targetTriple: "aarch64-linux-android",
74
- updatePlatform: "android-aarch64",
75
- },
76
- ios: {
77
- artifactGlobs: ["tauri/src-tauri/target/**/*.sig", "tauri/gen/apple/**/*.sig"],
78
- buildArgs: ["tauri", "ios", "build", "--target", "aarch64-apple-ios"],
79
- targetTriple: "aarch64-apple-ios",
80
- updatePlatform: "ios-aarch64",
81
- },
82
- linux: {
83
- artifactGlobs: [
84
- "tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/**/*.sig",
85
- "tauri/src-tauri/target/release/bundle/**/*.sig",
86
- "tauri/src-tauri/target/**/release/bundle/**/*.sig",
87
- ],
88
- buildArgs: ["tauri", "build", "--target", "x86_64-unknown-linux-gnu", "--", "--locked"],
89
- targetTriple: "x86_64-unknown-linux-gnu",
90
- updatePlatform: "linux-x86_64",
91
- },
92
- macos: {
93
- artifactGlobs: [
94
- "tauri/src-tauri/target/aarch64-apple-darwin/release/bundle/**/*.sig",
95
- "tauri/src-tauri/target/release/bundle/**/*.sig",
96
- "tauri/src-tauri/target/**/release/bundle/**/*.sig",
97
- ],
98
- buildArgs: ["tauri", "build", "--target", "aarch64-apple-darwin", "--", "--locked"],
99
- targetTriple: "aarch64-apple-darwin",
100
- updatePlatform: "darwin-aarch64",
101
- },
102
- windows: {
103
- artifactGlobs: [
104
- "tauri/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/**/*.sig",
105
- "tauri/src-tauri/target/release/bundle/**/*.sig",
106
- "tauri/src-tauri/target/**/release/bundle/**/*.sig",
107
- ],
108
- buildArgs: ["tauri", "build", "--target", "x86_64-pc-windows-msvc", "--", "--locked"],
109
- targetTriple: "x86_64-pc-windows-msvc",
110
- updatePlatform: "windows-x86_64",
111
- },
112
- }
113
-
114
- export function parseTauriReleasePlatformList(value: string | undefined) {
115
- if (!value) {
116
- return []
117
- }
118
-
119
- return value
120
- .split(/[,\s]+/u)
121
- .filter(Boolean)
122
- .map((platform) => parseTauriReleasePlatform(platform))
123
- }
124
-
125
- export function parseTauriReleasePlatform(value: string): TauriReleasePlatform {
126
- if (isTauriReleasePlatform(value)) {
127
- return value
128
- }
129
-
130
- throw new Error(
131
- `Invalid Tauri release platform "${value}". Expected one of: ${tauriReleasePlatforms.join(", ")}.`,
132
- )
133
- }
134
-
135
- export function resolveTauriReleasePlan({
136
- args,
137
- config = {},
138
- env,
139
- }: {
140
- args: TauriReleaseArgs
141
- config?: TauriReleaseConfig
142
- env: NodeJS.ProcessEnv
143
- }): TauriReleasePlan {
144
- const localPlatforms = uniquePlatforms([
145
- ...(config.localPlatforms ?? []),
146
- ...parseTauriReleasePlatformList(env.TAURI_RELEASE_LOCAL_PLATFORMS),
147
- ...args.tauriLocalPlatforms,
148
- ])
149
- const workflowPlatforms = uniquePlatforms([
150
- ...(config.workflowPlatforms ?? []),
151
- ...parseTauriReleasePlatformList(env.TAURI_RELEASE_WORKFLOW_PLATFORMS),
152
- ...args.tauriWorkflowPlatforms,
153
- ])
154
- const duplicatedPlatforms = localPlatforms.filter((platform) =>
155
- workflowPlatforms.includes(platform),
156
- )
157
-
158
- if (duplicatedPlatforms.length > 0) {
159
- throw new Error(
160
- `Build each Tauri release platform either locally or through the workflow, not both: ${duplicatedPlatforms.join(", ")}.`,
161
- )
162
- }
163
-
164
- return {
165
- artifactDownloadDir:
166
- env.TAURI_RELEASE_ARTIFACT_DOWNLOAD_DIR ??
167
- config.artifactDownloadDir ??
168
- ".coreframe/tauri-artifacts",
169
- b2ApplicationKey: env.B2_APPLICATION_KEY ?? env.B2_SECRET_ACCESS_KEY,
170
- b2ApplicationKeyId: env.B2_APPLICATION_KEY_ID ?? env.B2_KEY_ID ?? env.B2_ACCESS_KEY_ID,
171
- b2Bucket: config.b2Bucket ?? env.B2_BUCKET,
172
- enabled: localPlatforms.length > 0 || workflowPlatforms.length > 0,
173
- latestManifestPath:
174
- env.TAURI_RELEASE_LATEST_MANIFEST_PATH ?? config.latestManifestPath ?? "latest.json",
175
- localPlatforms,
176
- platform: resolvePlatformConfig(config.platform, env),
177
- publicBaseUrl:
178
- config.publicBaseUrl ?? env.TAURI_RELEASE_PUBLIC_BASE_URL ?? env.UPDATE_PUBLIC_BASE_URL,
179
- publicPathPrefix:
180
- env.TAURI_RELEASE_PUBLIC_PATH_PREFIX ??
181
- config.publicPathPrefix ??
182
- env.UPDATE_PUBLIC_PATH_PREFIX ??
183
- "",
184
- releaseNotesPath: env.TAURI_RELEASE_NOTES_FILE ?? config.releaseNotesPath,
185
- releasePath: env.TAURI_RELEASE_PATH ?? config.releasePath ?? "releases/${version}",
186
- releaseUploadDir: env.TAURI_RELEASE_UPLOAD_DIR ?? config.releaseUploadDir ?? "release-upload",
187
- signingPrivateKey: env.TAURI_SIGNING_PRIVATE_KEY,
188
- workflowName:
189
- args.tauriWorkflowName ??
190
- env.TAURI_RELEASE_WORKFLOW_NAME ??
191
- config.workflowName ??
192
- defaultWorkflowName,
193
- workflowPlatforms,
194
- workflowPollIntervalMs:
195
- parsePositiveInteger(
196
- env.TAURI_RELEASE_WORKFLOW_POLL_SECONDS,
197
- "TAURI_RELEASE_WORKFLOW_POLL_SECONDS",
198
- ) * 1000 ||
199
- config.workflowPollIntervalMs ||
200
- defaultWorkflowPollIntervalMs,
201
- workflowRef: args.tauriWorkflowRef ?? env.TAURI_RELEASE_WORKFLOW_REF ?? config.workflowRef,
202
- workflowTimeoutMs:
203
- parsePositiveInteger(
204
- env.TAURI_RELEASE_WORKFLOW_TIMEOUT_MINUTES,
205
- "TAURI_RELEASE_WORKFLOW_TIMEOUT_MINUTES",
206
- ) * 60_000 ||
207
- config.workflowTimeoutMs ||
208
- defaultWorkflowTimeoutMs,
209
- }
210
- }
211
-
212
- export async function validateTauriReleasePlan(plan: TauriReleasePlan, cwd = process.cwd()) {
213
- if (!plan.enabled) {
214
- return
215
- }
216
-
217
- const missing = [
218
- ["TAURI_RELEASE_PUBLIC_BASE_URL", plan.publicBaseUrl],
219
- ["B2_APPLICATION_KEY_ID", plan.b2ApplicationKeyId],
220
- ["B2_APPLICATION_KEY", plan.b2ApplicationKey],
221
- ["B2_BUCKET", plan.b2Bucket],
222
- ].flatMap(([name, value]) => (value ? [] : [name]))
223
-
224
- if (missing.length > 0) {
225
- throw new Error(`Missing required Tauri release configuration: ${missing.join(", ")}`)
226
- }
227
-
228
- try {
229
- new URL(plan.publicBaseUrl as string)
230
- } catch {
231
- throw new Error(`Invalid TAURI_RELEASE_PUBLIC_BASE_URL: ${plan.publicBaseUrl}`)
232
- }
233
-
234
- const tauriConfigPath = join(cwd, "tauri/src-tauri/tauri.conf.json")
235
-
236
- if (!(await pathExists(tauriConfigPath))) {
237
- throw new Error(
238
- "Tauri release platforms were requested, but tauri/src-tauri/tauri.conf.json does not exist.",
239
- )
240
- }
241
-
242
- if (plan.localPlatforms.length > 0 && !plan.signingPrivateKey) {
243
- throw new Error("TAURI_SIGNING_PRIVATE_KEY is required for local Tauri release builds.")
244
- }
245
-
246
- if (plan.workflowPlatforms.length > 0) {
247
- await nanoSpawn(
248
- "gh",
249
- ["workflow", "view", plan.workflowName, "--ref", plan.workflowRef ?? (await currentGitRef())],
250
- {
251
- stdin: "ignore",
252
- },
253
- )
254
- }
255
-
256
- await nanoSpawn("b2", ["version"], { stdin: "ignore" })
257
- }
258
-
259
- export async function writeTauriReleaseMetadata(
260
- plan: TauriReleasePlan,
261
- version: string,
262
- cwd = process.cwd(),
263
- ): Promise<TauriReleaseMetadataSnapshot> {
264
- if (!plan.enabled) {
265
- return { files: [] }
266
- }
267
-
268
- const tauriConfigPath = join(cwd, "tauri/src-tauri/tauri.conf.json")
269
- const cargoManifestPath = join(cwd, "tauri/src-tauri/Cargo.toml")
270
- const cargoLockPath = join(cwd, "tauri/src-tauri/Cargo.lock")
271
- const tauriConfigSource = await readFile(tauriConfigPath, "utf8")
272
- const cargoManifestSource = await readFile(cargoManifestPath, "utf8")
273
- const packageName = readCargoPackageName(cargoManifestSource, cargoManifestPath)
274
- const files = [
275
- { path: tauriConfigPath, source: tauriConfigSource },
276
- { path: cargoManifestPath, source: cargoManifestSource },
277
- ]
278
- const nextFiles = []
279
-
280
- const tauriConfig = JSON.parse(tauriConfigSource) as {
281
- bundle?: Record<string, unknown>
282
- version?: string
283
- [key: string]: unknown
284
- }
285
-
286
- tauriConfig.version = version
287
- tauriConfig.bundle = {
288
- ...(tauriConfig.bundle ?? {}),
289
- createUpdaterArtifacts: true,
290
- }
291
-
292
- nextFiles.push({ path: tauriConfigPath, source: `${JSON.stringify(tauriConfig, null, 2)}\n` })
293
- nextFiles.push({
294
- path: cargoManifestPath,
295
- source: replaceCargoPackageVersion(
296
- cargoManifestSource,
297
- packageName,
298
- version,
299
- cargoManifestPath,
300
- ),
301
- })
302
-
303
- if (await pathExists(cargoLockPath)) {
304
- const cargoLockSource = await readFile(cargoLockPath, "utf8")
305
-
306
- files.push({ path: cargoLockPath, source: cargoLockSource })
307
- nextFiles.push({
308
- path: cargoLockPath,
309
- source: replaceCargoLockPackageVersion(cargoLockSource, packageName, version, cargoLockPath),
310
- })
311
- }
312
-
313
- for (const file of nextFiles) {
314
- await writeFile(file.path, file.source)
315
- }
316
-
317
- return { files }
318
- }
319
-
320
- export async function restoreTauriReleaseMetadata(snapshot: TauriReleaseMetadataSnapshot) {
321
- for (const file of snapshot.files) {
322
- await writeFile(file.path, file.source)
323
- }
324
- }
325
-
326
- export async function prepareTauriRelease({
327
- env,
328
- plan,
329
- releaseName,
330
- version,
331
- }: {
332
- env: NodeJS.ProcessEnv
333
- plan: TauriReleasePlan
334
- releaseName: string
335
- version: string
336
- }): Promise<PreparedTauriRelease | undefined> {
337
- if (!plan.enabled) {
338
- return undefined
339
- }
340
-
341
- await resetGeneratedDirectory(plan.artifactDownloadDir)
342
- await resetGeneratedDirectory(plan.releaseUploadDir)
343
-
344
- const workflowRun =
345
- plan.workflowPlatforms.length > 0
346
- ? await startTauriWorkflowBuild({ plan, releaseName, version })
347
- : undefined
348
-
349
- for (const platform of plan.localPlatforms) {
350
- await buildLocalTauriPlatform(platform, plan, env)
351
- }
352
-
353
- if (workflowRun) {
354
- const runId = await waitForTauriWorkflowBuild(plan, workflowRun.runNameMarker)
355
- await nanoSpawn("gh", ["run", "download", String(runId), "--dir", plan.artifactDownloadDir], {
356
- stdio: "inherit",
357
- })
358
- }
359
-
360
- return createTauriReleaseUpload({ plan, version })
361
- }
362
-
363
- export async function uploadTauriReleaseToB2(
364
- plan: TauriReleasePlan,
365
- release: PreparedTauriRelease,
366
- ) {
367
- await nanoSpawn(
368
- "b2",
369
- ["account", "authorize", plan.b2ApplicationKeyId as string, plan.b2ApplicationKey as string],
370
- { stdin: "ignore" },
371
- )
372
-
373
- const files = await glob("**/*", { cwd: release.uploadDir, onlyFiles: true })
374
- const sortedFiles = files.sort((left, right) => {
375
- if (left === release.manifestPath) {
376
- return 1
377
- }
378
-
379
- if (right === release.manifestPath) {
380
- return -1
381
- }
382
-
383
- return left.localeCompare(right)
384
- })
385
-
386
- for (const file of sortedFiles) {
387
- await nanoSpawn(
388
- "b2",
389
- ["file", "upload", plan.b2Bucket as string, join(release.uploadDir, file), file],
390
- { stdin: "ignore" },
391
- )
392
- console.log(`Uploaded ${file}`)
393
- }
394
- }
395
-
396
- export function createTauriUpdateManifest({
397
- artifacts,
398
- notes,
399
- pubDate = new Date(),
400
- version,
401
- }: {
402
- artifacts: { signature: string; updatePlatform: string; url: string }[]
403
- notes?: string
404
- pubDate?: Date
405
- version: string
406
- }): TauriUpdateManifest {
407
- return {
408
- ...(notes ? { notes } : {}),
409
- platforms: Object.fromEntries(
410
- artifacts.map((artifact) => [
411
- artifact.updatePlatform,
412
- { signature: artifact.signature, url: artifact.url },
413
- ]),
414
- ),
415
- pub_date: pubDate.toISOString(),
416
- version,
417
- }
418
- }
419
-
420
- export async function createTauriReleaseUpload({
421
- plan,
422
- version,
423
- }: {
424
- plan: TauriReleasePlan
425
- version: string
426
- }): Promise<PreparedTauriRelease> {
427
- const releasePath = joinRemotePath(
428
- plan.publicPathPrefix,
429
- formatReleasePath(plan.releasePath, version),
430
- )
431
- const latestManifestPath = joinRemotePath(plan.publicPathPrefix, plan.latestManifestPath)
432
- const selectedPlatforms = [...plan.localPlatforms, ...plan.workflowPlatforms]
433
- const manifestArtifacts = []
434
-
435
- for (const platform of selectedPlatforms) {
436
- const platformConfig = plan.platform[platform]
437
- const sourceRoot = plan.localPlatforms.includes(platform)
438
- ? "."
439
- : join(plan.artifactDownloadDir, `tauri-${platform}-${version}`)
440
- const artifactPair = await findTauriUpdateArtifact({
441
- globs: plan.localPlatforms.includes(platform) ? platformConfig.artifactGlobs : ["**/*.sig"],
442
- sourceRoot,
443
- version,
444
- })
445
- const remoteDir = joinRemotePath(releasePath, platformConfig.updatePlatform)
446
- const artifactRemotePath = joinRemotePath(remoteDir, basename(artifactPair.artifactPath))
447
- const signatureRemotePath = joinRemotePath(remoteDir, basename(artifactPair.signaturePath))
448
- const uploadArtifactPath = join(plan.releaseUploadDir, ...artifactRemotePath.split("/"))
449
- const uploadSignaturePath = join(plan.releaseUploadDir, ...signatureRemotePath.split("/"))
450
- const signature = (await readFile(artifactPair.signaturePath, "utf8")).trim()
451
-
452
- await mkdir(dirname(uploadArtifactPath), { recursive: true })
453
- await cp(artifactPair.artifactPath, uploadArtifactPath)
454
- await cp(artifactPair.signaturePath, uploadSignaturePath)
455
- manifestArtifacts.push({
456
- signature,
457
- updatePlatform: platformConfig.updatePlatform,
458
- url: joinPublicUrl(plan.publicBaseUrl as string, artifactRemotePath),
459
- })
460
- }
461
-
462
- const notes = plan.releaseNotesPath ? await readFile(plan.releaseNotesPath, "utf8") : undefined
463
- const manifest = createTauriUpdateManifest({ artifacts: manifestArtifacts, notes, version })
464
- const manifestPath = join(plan.releaseUploadDir, ...latestManifestPath.split("/"))
465
-
466
- await mkdir(dirname(manifestPath), { recursive: true })
467
- await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
468
-
469
- return {
470
- manifestPath: latestManifestPath,
471
- uploadDir: plan.releaseUploadDir,
472
- }
473
- }
474
-
475
- async function startTauriWorkflowBuild({
476
- plan,
477
- releaseName,
478
- version,
479
- }: {
480
- plan: TauriReleasePlan
481
- releaseName: string
482
- version: string
483
- }) {
484
- const ref = plan.workflowRef ?? (await currentGitRef())
485
- const runNameMarker = `coreframe-${Date.now()}-${randomUUID().slice(0, 8)}`
486
-
487
- await nanoSpawn(
488
- "gh",
489
- [
490
- "workflow",
491
- "run",
492
- plan.workflowName,
493
- "--ref",
494
- ref,
495
- "-f",
496
- `release_name=${releaseName}`,
497
- "-f",
498
- `run_id=${runNameMarker}`,
499
- "-f",
500
- `version=${version}`,
501
- ...tauriReleasePlatforms.flatMap((platform) => [
502
- "-f",
503
- `${platform}=${plan.workflowPlatforms.includes(platform) ? "true" : "false"}`,
504
- ]),
505
- ],
506
- { stdin: "ignore" },
507
- )
508
-
509
- return { runNameMarker }
510
- }
511
-
512
- async function waitForTauriWorkflowBuild(plan: TauriReleasePlan, runNameMarker: string) {
513
- const deadline = Date.now() + plan.workflowTimeoutMs
514
-
515
- while (Date.now() < deadline) {
516
- const run = await findTauriWorkflowRun(plan, runNameMarker)
517
-
518
- if (run?.status === "completed") {
519
- if (run.conclusion === "success") {
520
- return run.databaseId
521
- }
522
-
523
- throw new Error(`Tauri release workflow ${run.databaseId} finished with ${run.conclusion}.`)
524
- }
525
-
526
- await delay(plan.workflowPollIntervalMs)
527
- }
528
-
529
- throw new Error(
530
- `Timed out waiting for Tauri release workflow after ${Math.round(plan.workflowTimeoutMs / 60_000)} minutes.`,
531
- )
532
- }
533
-
534
- async function findTauriWorkflowRun(plan: TauriReleasePlan, runNameMarker: string) {
535
- const { stdout } = await nanoSpawn(
536
- "gh",
537
- [
538
- "run",
539
- "list",
540
- "--workflow",
541
- plan.workflowName,
542
- "--event",
543
- "workflow_dispatch",
544
- "--json",
545
- "conclusion,databaseId,displayTitle,status",
546
- "--limit",
547
- "50",
548
- ],
549
- { stdin: "ignore" },
550
- )
551
- const runs = JSON.parse(stdout) as {
552
- conclusion: string | null
553
- databaseId: number
554
- displayTitle: string
555
- status: string
556
- }[]
557
-
558
- return runs.find((run) => run.displayTitle.includes(runNameMarker))
559
- }
560
-
561
- async function buildLocalTauriPlatform(
562
- platform: TauriReleasePlatform,
563
- plan: TauriReleasePlan,
564
- env: NodeJS.ProcessEnv,
565
- ) {
566
- await nanoSpawn("pnpm", plan.platform[platform].buildArgs, {
567
- env,
568
- stdio: "inherit",
569
- })
570
- }
571
-
572
- async function findTauriUpdateArtifact({
573
- globs,
574
- sourceRoot,
575
- version,
576
- }: {
577
- globs: string[]
578
- sourceRoot: string
579
- version: string
580
- }) {
581
- const signaturePaths = await glob(globs, { cwd: sourceRoot, absolute: true, onlyFiles: true })
582
- const candidatePairs = []
583
-
584
- for (const signaturePath of signaturePaths) {
585
- const artifactPath = signaturePath.replace(/\.sig$/u, "")
586
-
587
- if (await isFile(artifactPath)) {
588
- candidatePairs.push({ artifactPath, signaturePath })
589
- }
590
- }
591
-
592
- if (candidatePairs.length === 0) {
593
- throw new Error(`Could not find a Tauri updater artifact and .sig pair under ${sourceRoot}.`)
594
- }
595
-
596
- return candidatePairs.sort((left, right) => {
597
- const leftScore = artifactScore(left.artifactPath, version)
598
- const rightScore = artifactScore(right.artifactPath, version)
599
-
600
- return rightScore - leftScore || left.artifactPath.localeCompare(right.artifactPath)
601
- })[0]
602
- }
603
-
604
- function artifactScore(path: string, version: string) {
605
- let score = 0
606
-
607
- if (path.includes(version)) {
608
- score += 100
609
- }
610
-
611
- if (/\.(app\.tar\.gz|AppImage\.tar\.gz|zip)$/u.test(path)) {
612
- score += 10
613
- }
614
-
615
- if (/\.(exe|msi|dmg|AppImage|deb|rpm|apk|aab|ipa)$/u.test(path)) {
616
- score += 5
617
- }
618
-
619
- return score
620
- }
621
-
622
- function resolvePlatformConfig(
623
- config: TauriReleaseConfig["platform"] = {},
624
- env: NodeJS.ProcessEnv,
625
- ): Record<TauriReleasePlatform, ResolvedTauriReleasePlatformConfig> {
626
- return Object.fromEntries(
627
- tauriReleasePlatforms.map((platform) => {
628
- const override = config[platform] ?? {}
629
- const platformConfig = {
630
- ...defaultPlatformConfig[platform],
631
- ...override,
632
- }
633
- const envTargetTriple = env[`TAURI_RELEASE_${platform.toUpperCase()}_TARGET_TRIPLE`]
634
- const envUpdatePlatform = env[`TAURI_RELEASE_${platform.toUpperCase()}_UPDATE_PLATFORM`]
635
- const targetTriple = envTargetTriple ?? platformConfig.targetTriple
636
- const buildArgs = override.buildArgs
637
- ? override.buildArgs
638
- : defaultBuildArgs(platform, targetTriple)
639
-
640
- return [
641
- platform,
642
- {
643
- ...platformConfig,
644
- artifactGlobs:
645
- platformConfig.artifactGlobs ?? defaultPlatformConfig[platform].artifactGlobs,
646
- buildArgs,
647
- targetTriple,
648
- updatePlatform: envUpdatePlatform ?? platformConfig.updatePlatform,
649
- },
650
- ]
651
- }),
652
- ) as Record<TauriReleasePlatform, ResolvedTauriReleasePlatformConfig>
653
- }
654
-
655
- function defaultBuildArgs(platform: TauriReleasePlatform, targetTriple: string) {
656
- if (platform === "android") {
657
- return ["tauri", "android", "build", "--target", targetTriple]
658
- }
659
-
660
- if (platform === "ios") {
661
- return ["tauri", "ios", "build", "--target", targetTriple]
662
- }
663
-
664
- return ["tauri", "build", "--target", targetTriple, "--", "--locked"]
665
- }
666
-
667
- function readCargoPackageName(source: string, path: string) {
668
- const match = /^\[package\][\s\S]*?^name = "(?<name>[^"]+)"/mu.exec(source)
669
-
670
- if (!match?.groups?.name) {
671
- throw new Error(`${path} is missing a [package] name.`)
672
- }
673
-
674
- return match.groups.name
675
- }
676
-
677
- function replaceCargoPackageVersion(
678
- source: string,
679
- packageName: string,
680
- version: string,
681
- path: string,
682
- ) {
683
- const pattern = new RegExp(
684
- `(^\\[package\\][\\s\\S]*?^name = "${escapeRegExp(packageName)}"[\\s\\S]*?^version = ")[^"]+(")`,
685
- "mu",
686
- )
687
- const nextSource = source.replace(pattern, `$1${version}$2`)
688
-
689
- if (nextSource === source) {
690
- throw new Error(`${path} is missing a package version for ${packageName}.`)
691
- }
692
-
693
- return nextSource
694
- }
695
-
696
- function replaceCargoLockPackageVersion(
697
- source: string,
698
- packageName: string,
699
- version: string,
700
- path: string,
701
- ) {
702
- const pattern = new RegExp(
703
- `(\\[\\[package\\]\\]\\nname = "${escapeRegExp(packageName)}"\\nversion = ")[^"]+(")`,
704
- "u",
705
- )
706
- const nextSource = source.replace(pattern, `$1${version}$2`)
707
-
708
- if (nextSource === source) {
709
- throw new Error(`${path} is missing a package lock entry for ${packageName}.`)
710
- }
711
-
712
- return nextSource
713
- }
714
-
715
- async function currentGitRef() {
716
- const { stdout: branch } = await nanoSpawn("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
717
- stdin: "ignore",
718
- })
719
- const trimmedBranch = branch.trim()
720
-
721
- if (trimmedBranch !== "HEAD") {
722
- return trimmedBranch
723
- }
724
-
725
- const { stdout: commit } = await nanoSpawn("git", ["rev-parse", "HEAD"], {
726
- stdin: "ignore",
727
- })
728
-
729
- return commit.trim()
730
- }
731
-
732
- async function resetGeneratedDirectory(path: string) {
733
- assertSafeGeneratedDirectory(path)
734
- await rm(path, { force: true, recursive: true })
735
- await mkdir(path, { recursive: true })
736
- }
737
-
738
- function assertSafeGeneratedDirectory(path: string) {
739
- const resolvedPath = resolve(path)
740
- const relativePath = relative(process.cwd(), resolvedPath).split(sep).join("/")
741
-
742
- if (
743
- relativePath === "" ||
744
- relativePath === "." ||
745
- relativePath.startsWith("../") ||
746
- relativePath === "tauri" ||
747
- relativePath.startsWith("tauri/")
748
- ) {
749
- throw new Error(`Refusing to reset unsafe Tauri release directory: ${path}`)
750
- }
751
- }
752
-
753
- function formatReleasePath(template: string, version: string) {
754
- return template.replaceAll("${version}", version)
755
- }
756
-
757
- function joinPublicUrl(baseUrl: string, remotePath: string) {
758
- const url = new URL(baseUrl)
759
- const basePath = url.pathname.replace(/\/+$/u, "")
760
- const nextPath = joinRemotePath(basePath, remotePath)
761
-
762
- url.pathname = nextPath.startsWith("/") ? nextPath : `/${nextPath}`
763
-
764
- return url.toString()
765
- }
766
-
767
- function joinRemotePath(...parts: string[]) {
768
- return parts
769
- .flatMap((part) => part.split("/"))
770
- .map((part) => part.trim())
771
- .filter(Boolean)
772
- .join("/")
773
- }
774
-
775
- function parsePositiveInteger(value: string | undefined, name: string) {
776
- if (!value) {
777
- return 0
778
- }
779
-
780
- const parsed = Number(value)
781
-
782
- if (!Number.isInteger(parsed) || parsed <= 0) {
783
- throw new Error(`${name} must be a positive integer.`)
784
- }
785
-
786
- return parsed
787
- }
788
-
789
- function uniquePlatforms(platforms: TauriReleasePlatform[]) {
790
- return [...new Set(platforms)]
791
- }
792
-
793
- function isTauriReleasePlatform(value: string): value is TauriReleasePlatform {
794
- return (tauriReleasePlatforms as readonly string[]).includes(value)
795
- }
796
-
797
- async function pathExists(path: string) {
798
- try {
799
- await access(path, constants.F_OK)
800
- return true
801
- } catch {
802
- return false
803
- }
804
- }
805
-
806
- async function isFile(path: string) {
807
- try {
808
- return (await stat(path)).isFile()
809
- } catch {
810
- return false
811
- }
812
- }
813
-
814
- function escapeRegExp(value: string) {
815
- return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")
816
- }