@coreframe/scripts 0.0.0 → 0.1.0
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/bin/coreframe.js +35 -0
- package/config.test.ts +15 -0
- package/config.ts +9 -0
- package/coreframe.ts +272 -0
- package/db/client.ts +72 -0
- package/db/config.ts +41 -0
- package/db/drizzle-kit.test.ts +27 -0
- package/db/drizzle-kit.ts +39 -0
- package/db/ensure.ts +67 -0
- package/db/migrate-held.test.ts +81 -0
- package/db/migrate-held.ts +166 -0
- package/db/migrate.ts +39 -0
- package/db/project.ts +28 -0
- package/db/reset.ts +24 -0
- package/db/seed.ts +38 -0
- package/db/setup.ts +18 -0
- package/deploy/check-migrations.ts +31 -0
- package/deploy/checks.ts +38 -0
- package/deploy/config.test.ts +24 -0
- package/deploy/config.ts +68 -0
- package/deploy/migrations.test.ts +110 -0
- package/deploy/migrations.ts +317 -0
- package/deploy/pending-migrations.test.ts +93 -0
- package/deploy/pending-migrations.ts +45 -0
- package/deploy/tauri-release.test.ts +196 -0
- package/deploy/tauri-release.ts +816 -0
- package/deploy/version.test.ts +29 -0
- package/deploy/version.ts +65 -0
- package/deploy.test.ts +101 -0
- package/deploy.ts +346 -0
- package/dev.test.ts +153 -0
- package/dev.ts +155 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +3 -0
- package/dist/coreframe.d.ts +280 -0
- package/dist/coreframe.js +240 -0
- package/dist/db/client.d.ts +22 -0
- package/dist/db/client.js +29 -0
- package/dist/db/config.d.ts +21 -0
- package/dist/db/config.js +32 -0
- package/dist/db/drizzle-kit.d.ts +8 -0
- package/dist/db/drizzle-kit.js +22 -0
- package/dist/db/ensure.d.ts +2 -0
- package/dist/db/ensure.js +40 -0
- package/dist/db/migrate-held.d.ts +12 -0
- package/dist/db/migrate-held.js +119 -0
- package/dist/db/migrate.d.ts +2 -0
- package/dist/db/migrate.js +25 -0
- package/dist/db/project.d.ts +8 -0
- package/dist/db/project.js +9 -0
- package/dist/db/reset.d.ts +2 -0
- package/dist/db/reset.js +19 -0
- package/dist/db/seed.d.ts +3 -0
- package/dist/db/seed.js +23 -0
- package/dist/db/setup.d.ts +3 -0
- package/dist/db/setup.js +15 -0
- package/dist/deploy/check-migrations.d.ts +2 -0
- package/dist/deploy/check-migrations.js +18 -0
- package/dist/deploy/checks.d.ts +2 -0
- package/dist/deploy/checks.js +27 -0
- package/dist/deploy/config.d.ts +50 -0
- package/dist/deploy/config.js +9 -0
- package/dist/deploy/migrations.d.ts +21 -0
- package/dist/deploy/migrations.js +203 -0
- package/dist/deploy/pending-migrations.d.ts +5 -0
- package/dist/deploy/pending-migrations.js +30 -0
- package/dist/deploy/tauri-release.d.ts +85 -0
- package/dist/deploy/tauri-release.js +512 -0
- package/dist/deploy/version.d.ts +18 -0
- package/dist/deploy/version.js +41 -0
- package/dist/deploy.d.ts +17 -0
- package/dist/deploy.js +247 -0
- package/dist/dev.d.ts +13 -0
- package/dist/dev.js +96 -0
- package/dist/image-generator.d.ts +2 -0
- package/dist/image-generator.js +59 -0
- package/dist/project.d.ts +1 -0
- package/dist/project.js +13 -0
- package/dist/typecheck.d.ts +2 -0
- package/dist/typecheck.js +45 -0
- package/dist/upgrade/check-skill-current.d.ts +6 -0
- package/dist/upgrade/check-skill-current.js +88 -0
- package/dist/upgrade/ensure-template-remote.d.ts +6 -0
- package/dist/upgrade/ensure-template-remote.js +57 -0
- package/dist/upgrade/fast-forward-template-files.d.ts +7 -0
- package/dist/upgrade/fast-forward-template-files.js +280 -0
- package/image-generator.ts +84 -0
- package/package.json +48 -8
- package/project.ts +6 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +20 -0
- package/typecheck.ts +55 -0
- package/upgrade/check-skill-current.ts +112 -0
- package/upgrade/ensure-template-remote.ts +79 -0
- package/upgrade/fast-forward-template-files.ts +408 -0
- package/readme.md +0 -1
|
@@ -0,0 +1,512 @@
|
|
|
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
|
+
import nanoSpawn from "nano-spawn";
|
|
7
|
+
import { glob } from "tinyglobby";
|
|
8
|
+
import { tauriReleasePlatforms } from "./config.js";
|
|
9
|
+
const defaultWorkflowName = "tauri-release-artifacts.yml";
|
|
10
|
+
const defaultWorkflowPollIntervalMs = 15_000;
|
|
11
|
+
const defaultWorkflowTimeoutMs = 60 * 60_000;
|
|
12
|
+
const defaultPlatformConfig = {
|
|
13
|
+
android: {
|
|
14
|
+
artifactGlobs: ["tauri/src-tauri/target/**/*.sig", "tauri/gen/android/**/*.sig"],
|
|
15
|
+
buildArgs: ["tauri", "android", "build", "--target", "aarch64-linux-android"],
|
|
16
|
+
targetTriple: "aarch64-linux-android",
|
|
17
|
+
updatePlatform: "android-aarch64",
|
|
18
|
+
},
|
|
19
|
+
ios: {
|
|
20
|
+
artifactGlobs: ["tauri/src-tauri/target/**/*.sig", "tauri/gen/apple/**/*.sig"],
|
|
21
|
+
buildArgs: ["tauri", "ios", "build", "--target", "aarch64-apple-ios"],
|
|
22
|
+
targetTriple: "aarch64-apple-ios",
|
|
23
|
+
updatePlatform: "ios-aarch64",
|
|
24
|
+
},
|
|
25
|
+
linux: {
|
|
26
|
+
artifactGlobs: [
|
|
27
|
+
"tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/**/*.sig",
|
|
28
|
+
"tauri/src-tauri/target/release/bundle/**/*.sig",
|
|
29
|
+
"tauri/src-tauri/target/**/release/bundle/**/*.sig",
|
|
30
|
+
],
|
|
31
|
+
buildArgs: ["tauri", "build", "--target", "x86_64-unknown-linux-gnu", "--", "--locked"],
|
|
32
|
+
targetTriple: "x86_64-unknown-linux-gnu",
|
|
33
|
+
updatePlatform: "linux-x86_64",
|
|
34
|
+
},
|
|
35
|
+
macos: {
|
|
36
|
+
artifactGlobs: [
|
|
37
|
+
"tauri/src-tauri/target/aarch64-apple-darwin/release/bundle/**/*.sig",
|
|
38
|
+
"tauri/src-tauri/target/release/bundle/**/*.sig",
|
|
39
|
+
"tauri/src-tauri/target/**/release/bundle/**/*.sig",
|
|
40
|
+
],
|
|
41
|
+
buildArgs: ["tauri", "build", "--target", "aarch64-apple-darwin", "--", "--locked"],
|
|
42
|
+
targetTriple: "aarch64-apple-darwin",
|
|
43
|
+
updatePlatform: "darwin-aarch64",
|
|
44
|
+
},
|
|
45
|
+
windows: {
|
|
46
|
+
artifactGlobs: [
|
|
47
|
+
"tauri/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/**/*.sig",
|
|
48
|
+
"tauri/src-tauri/target/release/bundle/**/*.sig",
|
|
49
|
+
"tauri/src-tauri/target/**/release/bundle/**/*.sig",
|
|
50
|
+
],
|
|
51
|
+
buildArgs: ["tauri", "build", "--target", "x86_64-pc-windows-msvc", "--", "--locked"],
|
|
52
|
+
targetTriple: "x86_64-pc-windows-msvc",
|
|
53
|
+
updatePlatform: "windows-x86_64",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
export function parseTauriReleasePlatformList(value) {
|
|
57
|
+
if (!value) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return value
|
|
61
|
+
.split(/[,\s]+/u)
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((platform) => parseTauriReleasePlatform(platform));
|
|
64
|
+
}
|
|
65
|
+
export function parseTauriReleasePlatform(value) {
|
|
66
|
+
if (isTauriReleasePlatform(value)) {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Invalid Tauri release platform "${value}". Expected one of: ${tauriReleasePlatforms.join(", ")}.`);
|
|
70
|
+
}
|
|
71
|
+
export function resolveTauriReleasePlan({ args, config = {}, env, }) {
|
|
72
|
+
const localPlatforms = uniquePlatforms([
|
|
73
|
+
...(config.localPlatforms ?? []),
|
|
74
|
+
...parseTauriReleasePlatformList(env.TAURI_RELEASE_LOCAL_PLATFORMS),
|
|
75
|
+
...args.tauriLocalPlatforms,
|
|
76
|
+
]);
|
|
77
|
+
const workflowPlatforms = uniquePlatforms([
|
|
78
|
+
...(config.workflowPlatforms ?? []),
|
|
79
|
+
...parseTauriReleasePlatformList(env.TAURI_RELEASE_WORKFLOW_PLATFORMS),
|
|
80
|
+
...args.tauriWorkflowPlatforms,
|
|
81
|
+
]);
|
|
82
|
+
const duplicatedPlatforms = localPlatforms.filter((platform) => workflowPlatforms.includes(platform));
|
|
83
|
+
if (duplicatedPlatforms.length > 0) {
|
|
84
|
+
throw new Error(`Build each Tauri release platform either locally or through the workflow, not both: ${duplicatedPlatforms.join(", ")}.`);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
artifactDownloadDir: env.TAURI_RELEASE_ARTIFACT_DOWNLOAD_DIR ??
|
|
88
|
+
config.artifactDownloadDir ??
|
|
89
|
+
".coreframe/tauri-artifacts",
|
|
90
|
+
b2ApplicationKey: env.B2_APPLICATION_KEY ?? env.B2_SECRET_ACCESS_KEY,
|
|
91
|
+
b2ApplicationKeyId: env.B2_APPLICATION_KEY_ID ?? env.B2_KEY_ID ?? env.B2_ACCESS_KEY_ID,
|
|
92
|
+
b2Bucket: config.b2Bucket ?? env.B2_BUCKET,
|
|
93
|
+
enabled: localPlatforms.length > 0 || workflowPlatforms.length > 0,
|
|
94
|
+
latestManifestPath: env.TAURI_RELEASE_LATEST_MANIFEST_PATH ?? config.latestManifestPath ?? "latest.json",
|
|
95
|
+
localPlatforms,
|
|
96
|
+
platform: resolvePlatformConfig(config.platform, env),
|
|
97
|
+
publicBaseUrl: config.publicBaseUrl ?? env.TAURI_RELEASE_PUBLIC_BASE_URL ?? env.UPDATE_PUBLIC_BASE_URL,
|
|
98
|
+
publicPathPrefix: env.TAURI_RELEASE_PUBLIC_PATH_PREFIX ??
|
|
99
|
+
config.publicPathPrefix ??
|
|
100
|
+
env.UPDATE_PUBLIC_PATH_PREFIX ??
|
|
101
|
+
"",
|
|
102
|
+
releaseNotesPath: env.TAURI_RELEASE_NOTES_FILE ?? config.releaseNotesPath,
|
|
103
|
+
releasePath: env.TAURI_RELEASE_PATH ?? config.releasePath ?? "releases/${version}",
|
|
104
|
+
releaseUploadDir: env.TAURI_RELEASE_UPLOAD_DIR ?? config.releaseUploadDir ?? "release-upload",
|
|
105
|
+
signingPrivateKey: env.TAURI_SIGNING_PRIVATE_KEY,
|
|
106
|
+
workflowName: args.tauriWorkflowName ??
|
|
107
|
+
env.TAURI_RELEASE_WORKFLOW_NAME ??
|
|
108
|
+
config.workflowName ??
|
|
109
|
+
defaultWorkflowName,
|
|
110
|
+
workflowPlatforms,
|
|
111
|
+
workflowPollIntervalMs: parsePositiveInteger(env.TAURI_RELEASE_WORKFLOW_POLL_SECONDS, "TAURI_RELEASE_WORKFLOW_POLL_SECONDS") * 1000 ||
|
|
112
|
+
config.workflowPollIntervalMs ||
|
|
113
|
+
defaultWorkflowPollIntervalMs,
|
|
114
|
+
workflowRef: args.tauriWorkflowRef ?? env.TAURI_RELEASE_WORKFLOW_REF ?? config.workflowRef,
|
|
115
|
+
workflowTimeoutMs: parsePositiveInteger(env.TAURI_RELEASE_WORKFLOW_TIMEOUT_MINUTES, "TAURI_RELEASE_WORKFLOW_TIMEOUT_MINUTES") * 60_000 ||
|
|
116
|
+
config.workflowTimeoutMs ||
|
|
117
|
+
defaultWorkflowTimeoutMs,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export async function validateTauriReleasePlan(plan, cwd = process.cwd()) {
|
|
121
|
+
if (!plan.enabled) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const missing = [
|
|
125
|
+
["TAURI_RELEASE_PUBLIC_BASE_URL", plan.publicBaseUrl],
|
|
126
|
+
["B2_APPLICATION_KEY_ID", plan.b2ApplicationKeyId],
|
|
127
|
+
["B2_APPLICATION_KEY", plan.b2ApplicationKey],
|
|
128
|
+
["B2_BUCKET", plan.b2Bucket],
|
|
129
|
+
].flatMap(([name, value]) => (value ? [] : [name]));
|
|
130
|
+
if (missing.length > 0) {
|
|
131
|
+
throw new Error(`Missing required Tauri release configuration: ${missing.join(", ")}`);
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
new URL(plan.publicBaseUrl);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
throw new Error(`Invalid TAURI_RELEASE_PUBLIC_BASE_URL: ${plan.publicBaseUrl}`);
|
|
138
|
+
}
|
|
139
|
+
const tauriConfigPath = join(cwd, "tauri/src-tauri/tauri.conf.json");
|
|
140
|
+
if (!(await pathExists(tauriConfigPath))) {
|
|
141
|
+
throw new Error("Tauri release platforms were requested, but tauri/src-tauri/tauri.conf.json does not exist.");
|
|
142
|
+
}
|
|
143
|
+
if (plan.localPlatforms.length > 0 && !plan.signingPrivateKey) {
|
|
144
|
+
throw new Error("TAURI_SIGNING_PRIVATE_KEY is required for local Tauri release builds.");
|
|
145
|
+
}
|
|
146
|
+
if (plan.workflowPlatforms.length > 0) {
|
|
147
|
+
await nanoSpawn("gh", ["workflow", "view", plan.workflowName, "--ref", plan.workflowRef ?? (await currentGitRef())], {
|
|
148
|
+
stdin: "ignore",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
await nanoSpawn("b2", ["version"], { stdin: "ignore" });
|
|
152
|
+
}
|
|
153
|
+
export async function writeTauriReleaseMetadata(plan, version, cwd = process.cwd()) {
|
|
154
|
+
if (!plan.enabled) {
|
|
155
|
+
return { files: [] };
|
|
156
|
+
}
|
|
157
|
+
const tauriConfigPath = join(cwd, "tauri/src-tauri/tauri.conf.json");
|
|
158
|
+
const cargoManifestPath = join(cwd, "tauri/src-tauri/Cargo.toml");
|
|
159
|
+
const cargoLockPath = join(cwd, "tauri/src-tauri/Cargo.lock");
|
|
160
|
+
const tauriConfigSource = await readFile(tauriConfigPath, "utf8");
|
|
161
|
+
const cargoManifestSource = await readFile(cargoManifestPath, "utf8");
|
|
162
|
+
const packageName = readCargoPackageName(cargoManifestSource, cargoManifestPath);
|
|
163
|
+
const files = [
|
|
164
|
+
{ path: tauriConfigPath, source: tauriConfigSource },
|
|
165
|
+
{ path: cargoManifestPath, source: cargoManifestSource },
|
|
166
|
+
];
|
|
167
|
+
const nextFiles = [];
|
|
168
|
+
const tauriConfig = JSON.parse(tauriConfigSource);
|
|
169
|
+
tauriConfig.version = version;
|
|
170
|
+
tauriConfig.bundle = {
|
|
171
|
+
...(tauriConfig.bundle ?? {}),
|
|
172
|
+
createUpdaterArtifacts: true,
|
|
173
|
+
};
|
|
174
|
+
nextFiles.push({ path: tauriConfigPath, source: `${JSON.stringify(tauriConfig, null, 2)}\n` });
|
|
175
|
+
nextFiles.push({
|
|
176
|
+
path: cargoManifestPath,
|
|
177
|
+
source: replaceCargoPackageVersion(cargoManifestSource, packageName, version, cargoManifestPath),
|
|
178
|
+
});
|
|
179
|
+
if (await pathExists(cargoLockPath)) {
|
|
180
|
+
const cargoLockSource = await readFile(cargoLockPath, "utf8");
|
|
181
|
+
files.push({ path: cargoLockPath, source: cargoLockSource });
|
|
182
|
+
nextFiles.push({
|
|
183
|
+
path: cargoLockPath,
|
|
184
|
+
source: replaceCargoLockPackageVersion(cargoLockSource, packageName, version, cargoLockPath),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
for (const file of nextFiles) {
|
|
188
|
+
await writeFile(file.path, file.source);
|
|
189
|
+
}
|
|
190
|
+
return { files };
|
|
191
|
+
}
|
|
192
|
+
export async function restoreTauriReleaseMetadata(snapshot) {
|
|
193
|
+
for (const file of snapshot.files) {
|
|
194
|
+
await writeFile(file.path, file.source);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export async function prepareTauriRelease({ env, plan, releaseName, version, }) {
|
|
198
|
+
if (!plan.enabled) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
await resetGeneratedDirectory(plan.artifactDownloadDir);
|
|
202
|
+
await resetGeneratedDirectory(plan.releaseUploadDir);
|
|
203
|
+
const workflowRun = plan.workflowPlatforms.length > 0
|
|
204
|
+
? await startTauriWorkflowBuild({ plan, releaseName, version })
|
|
205
|
+
: undefined;
|
|
206
|
+
for (const platform of plan.localPlatforms) {
|
|
207
|
+
await buildLocalTauriPlatform(platform, plan, env);
|
|
208
|
+
}
|
|
209
|
+
if (workflowRun) {
|
|
210
|
+
const runId = await waitForTauriWorkflowBuild(plan, workflowRun.runNameMarker);
|
|
211
|
+
await nanoSpawn("gh", ["run", "download", String(runId), "--dir", plan.artifactDownloadDir], {
|
|
212
|
+
stdio: "inherit",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return createTauriReleaseUpload({ plan, version });
|
|
216
|
+
}
|
|
217
|
+
export async function uploadTauriReleaseToB2(plan, release) {
|
|
218
|
+
await nanoSpawn("b2", ["account", "authorize", plan.b2ApplicationKeyId, plan.b2ApplicationKey], { stdin: "ignore" });
|
|
219
|
+
const files = await glob("**/*", { cwd: release.uploadDir, onlyFiles: true });
|
|
220
|
+
const sortedFiles = files.sort((left, right) => {
|
|
221
|
+
if (left === release.manifestPath) {
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
if (right === release.manifestPath) {
|
|
225
|
+
return -1;
|
|
226
|
+
}
|
|
227
|
+
return left.localeCompare(right);
|
|
228
|
+
});
|
|
229
|
+
for (const file of sortedFiles) {
|
|
230
|
+
await nanoSpawn("b2", ["file", "upload", plan.b2Bucket, join(release.uploadDir, file), file], { stdin: "ignore" });
|
|
231
|
+
console.log(`Uploaded ${file}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export function createTauriUpdateManifest({ artifacts, notes, pubDate = new Date(), version, }) {
|
|
235
|
+
return {
|
|
236
|
+
...(notes ? { notes } : {}),
|
|
237
|
+
platforms: Object.fromEntries(artifacts.map((artifact) => [
|
|
238
|
+
artifact.updatePlatform,
|
|
239
|
+
{ signature: artifact.signature, url: artifact.url },
|
|
240
|
+
])),
|
|
241
|
+
pub_date: pubDate.toISOString(),
|
|
242
|
+
version,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
export async function createTauriReleaseUpload({ plan, version, }) {
|
|
246
|
+
const releasePath = joinRemotePath(plan.publicPathPrefix, formatReleasePath(plan.releasePath, version));
|
|
247
|
+
const latestManifestPath = joinRemotePath(plan.publicPathPrefix, plan.latestManifestPath);
|
|
248
|
+
const selectedPlatforms = [...plan.localPlatforms, ...plan.workflowPlatforms];
|
|
249
|
+
const manifestArtifacts = [];
|
|
250
|
+
for (const platform of selectedPlatforms) {
|
|
251
|
+
const platformConfig = plan.platform[platform];
|
|
252
|
+
const sourceRoot = plan.localPlatforms.includes(platform)
|
|
253
|
+
? "."
|
|
254
|
+
: join(plan.artifactDownloadDir, `tauri-${platform}-${version}`);
|
|
255
|
+
const artifactPair = await findTauriUpdateArtifact({
|
|
256
|
+
globs: plan.localPlatforms.includes(platform) ? platformConfig.artifactGlobs : ["**/*.sig"],
|
|
257
|
+
sourceRoot,
|
|
258
|
+
version,
|
|
259
|
+
});
|
|
260
|
+
const remoteDir = joinRemotePath(releasePath, platformConfig.updatePlatform);
|
|
261
|
+
const artifactRemotePath = joinRemotePath(remoteDir, basename(artifactPair.artifactPath));
|
|
262
|
+
const signatureRemotePath = joinRemotePath(remoteDir, basename(artifactPair.signaturePath));
|
|
263
|
+
const uploadArtifactPath = join(plan.releaseUploadDir, ...artifactRemotePath.split("/"));
|
|
264
|
+
const uploadSignaturePath = join(plan.releaseUploadDir, ...signatureRemotePath.split("/"));
|
|
265
|
+
const signature = (await readFile(artifactPair.signaturePath, "utf8")).trim();
|
|
266
|
+
await mkdir(dirname(uploadArtifactPath), { recursive: true });
|
|
267
|
+
await cp(artifactPair.artifactPath, uploadArtifactPath);
|
|
268
|
+
await cp(artifactPair.signaturePath, uploadSignaturePath);
|
|
269
|
+
manifestArtifacts.push({
|
|
270
|
+
signature,
|
|
271
|
+
updatePlatform: platformConfig.updatePlatform,
|
|
272
|
+
url: joinPublicUrl(plan.publicBaseUrl, artifactRemotePath),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const notes = plan.releaseNotesPath ? await readFile(plan.releaseNotesPath, "utf8") : undefined;
|
|
276
|
+
const manifest = createTauriUpdateManifest({ artifacts: manifestArtifacts, notes, version });
|
|
277
|
+
const manifestPath = join(plan.releaseUploadDir, ...latestManifestPath.split("/"));
|
|
278
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
279
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
280
|
+
return {
|
|
281
|
+
manifestPath: latestManifestPath,
|
|
282
|
+
uploadDir: plan.releaseUploadDir,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
async function startTauriWorkflowBuild({ plan, releaseName, version, }) {
|
|
286
|
+
const ref = plan.workflowRef ?? (await currentGitRef());
|
|
287
|
+
const runNameMarker = `coreframe-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
288
|
+
await nanoSpawn("gh", [
|
|
289
|
+
"workflow",
|
|
290
|
+
"run",
|
|
291
|
+
plan.workflowName,
|
|
292
|
+
"--ref",
|
|
293
|
+
ref,
|
|
294
|
+
"-f",
|
|
295
|
+
`release_name=${releaseName}`,
|
|
296
|
+
"-f",
|
|
297
|
+
`run_id=${runNameMarker}`,
|
|
298
|
+
"-f",
|
|
299
|
+
`version=${version}`,
|
|
300
|
+
...tauriReleasePlatforms.flatMap((platform) => [
|
|
301
|
+
"-f",
|
|
302
|
+
`${platform}=${plan.workflowPlatforms.includes(platform) ? "true" : "false"}`,
|
|
303
|
+
]),
|
|
304
|
+
], { stdin: "ignore" });
|
|
305
|
+
return { runNameMarker };
|
|
306
|
+
}
|
|
307
|
+
async function waitForTauriWorkflowBuild(plan, runNameMarker) {
|
|
308
|
+
const deadline = Date.now() + plan.workflowTimeoutMs;
|
|
309
|
+
while (Date.now() < deadline) {
|
|
310
|
+
const run = await findTauriWorkflowRun(plan, runNameMarker);
|
|
311
|
+
if (run?.status === "completed") {
|
|
312
|
+
if (run.conclusion === "success") {
|
|
313
|
+
return run.databaseId;
|
|
314
|
+
}
|
|
315
|
+
throw new Error(`Tauri release workflow ${run.databaseId} finished with ${run.conclusion}.`);
|
|
316
|
+
}
|
|
317
|
+
await delay(plan.workflowPollIntervalMs);
|
|
318
|
+
}
|
|
319
|
+
throw new Error(`Timed out waiting for Tauri release workflow after ${Math.round(plan.workflowTimeoutMs / 60_000)} minutes.`);
|
|
320
|
+
}
|
|
321
|
+
async function findTauriWorkflowRun(plan, runNameMarker) {
|
|
322
|
+
const { stdout } = await nanoSpawn("gh", [
|
|
323
|
+
"run",
|
|
324
|
+
"list",
|
|
325
|
+
"--workflow",
|
|
326
|
+
plan.workflowName,
|
|
327
|
+
"--event",
|
|
328
|
+
"workflow_dispatch",
|
|
329
|
+
"--json",
|
|
330
|
+
"conclusion,databaseId,displayTitle,status",
|
|
331
|
+
"--limit",
|
|
332
|
+
"50",
|
|
333
|
+
], { stdin: "ignore" });
|
|
334
|
+
const runs = JSON.parse(stdout);
|
|
335
|
+
return runs.find((run) => run.displayTitle.includes(runNameMarker));
|
|
336
|
+
}
|
|
337
|
+
async function buildLocalTauriPlatform(platform, plan, env) {
|
|
338
|
+
await nanoSpawn("pnpm", plan.platform[platform].buildArgs, {
|
|
339
|
+
env,
|
|
340
|
+
stdio: "inherit",
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
async function findTauriUpdateArtifact({ globs, sourceRoot, version, }) {
|
|
344
|
+
const signaturePaths = await glob(globs, { cwd: sourceRoot, absolute: true, onlyFiles: true });
|
|
345
|
+
const candidatePairs = [];
|
|
346
|
+
for (const signaturePath of signaturePaths) {
|
|
347
|
+
const artifactPath = signaturePath.replace(/\.sig$/u, "");
|
|
348
|
+
if (await isFile(artifactPath)) {
|
|
349
|
+
candidatePairs.push({ artifactPath, signaturePath });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (candidatePairs.length === 0) {
|
|
353
|
+
throw new Error(`Could not find a Tauri updater artifact and .sig pair under ${sourceRoot}.`);
|
|
354
|
+
}
|
|
355
|
+
return candidatePairs.sort((left, right) => {
|
|
356
|
+
const leftScore = artifactScore(left.artifactPath, version);
|
|
357
|
+
const rightScore = artifactScore(right.artifactPath, version);
|
|
358
|
+
return rightScore - leftScore || left.artifactPath.localeCompare(right.artifactPath);
|
|
359
|
+
})[0];
|
|
360
|
+
}
|
|
361
|
+
function artifactScore(path, version) {
|
|
362
|
+
let score = 0;
|
|
363
|
+
if (path.includes(version)) {
|
|
364
|
+
score += 100;
|
|
365
|
+
}
|
|
366
|
+
if (/\.(app\.tar\.gz|AppImage\.tar\.gz|zip)$/u.test(path)) {
|
|
367
|
+
score += 10;
|
|
368
|
+
}
|
|
369
|
+
if (/\.(exe|msi|dmg|AppImage|deb|rpm|apk|aab|ipa)$/u.test(path)) {
|
|
370
|
+
score += 5;
|
|
371
|
+
}
|
|
372
|
+
return score;
|
|
373
|
+
}
|
|
374
|
+
function resolvePlatformConfig(config = {}, env) {
|
|
375
|
+
return Object.fromEntries(tauriReleasePlatforms.map((platform) => {
|
|
376
|
+
const override = config[platform] ?? {};
|
|
377
|
+
const platformConfig = {
|
|
378
|
+
...defaultPlatformConfig[platform],
|
|
379
|
+
...override,
|
|
380
|
+
};
|
|
381
|
+
const envTargetTriple = env[`TAURI_RELEASE_${platform.toUpperCase()}_TARGET_TRIPLE`];
|
|
382
|
+
const envUpdatePlatform = env[`TAURI_RELEASE_${platform.toUpperCase()}_UPDATE_PLATFORM`];
|
|
383
|
+
const targetTriple = envTargetTriple ?? platformConfig.targetTriple;
|
|
384
|
+
const buildArgs = override.buildArgs
|
|
385
|
+
? override.buildArgs
|
|
386
|
+
: defaultBuildArgs(platform, targetTriple);
|
|
387
|
+
return [
|
|
388
|
+
platform,
|
|
389
|
+
{
|
|
390
|
+
...platformConfig,
|
|
391
|
+
artifactGlobs: platformConfig.artifactGlobs ?? defaultPlatformConfig[platform].artifactGlobs,
|
|
392
|
+
buildArgs,
|
|
393
|
+
targetTriple,
|
|
394
|
+
updatePlatform: envUpdatePlatform ?? platformConfig.updatePlatform,
|
|
395
|
+
},
|
|
396
|
+
];
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
function defaultBuildArgs(platform, targetTriple) {
|
|
400
|
+
if (platform === "android") {
|
|
401
|
+
return ["tauri", "android", "build", "--target", targetTriple];
|
|
402
|
+
}
|
|
403
|
+
if (platform === "ios") {
|
|
404
|
+
return ["tauri", "ios", "build", "--target", targetTriple];
|
|
405
|
+
}
|
|
406
|
+
return ["tauri", "build", "--target", targetTriple, "--", "--locked"];
|
|
407
|
+
}
|
|
408
|
+
function readCargoPackageName(source, path) {
|
|
409
|
+
const match = /^\[package\][\s\S]*?^name = "(?<name>[^"]+)"/mu.exec(source);
|
|
410
|
+
if (!match?.groups?.name) {
|
|
411
|
+
throw new Error(`${path} is missing a [package] name.`);
|
|
412
|
+
}
|
|
413
|
+
return match.groups.name;
|
|
414
|
+
}
|
|
415
|
+
function replaceCargoPackageVersion(source, packageName, version, path) {
|
|
416
|
+
const pattern = new RegExp(`(^\\[package\\][\\s\\S]*?^name = "${escapeRegExp(packageName)}"[\\s\\S]*?^version = ")[^"]+(")`, "mu");
|
|
417
|
+
const nextSource = source.replace(pattern, `$1${version}$2`);
|
|
418
|
+
if (nextSource === source) {
|
|
419
|
+
throw new Error(`${path} is missing a package version for ${packageName}.`);
|
|
420
|
+
}
|
|
421
|
+
return nextSource;
|
|
422
|
+
}
|
|
423
|
+
function replaceCargoLockPackageVersion(source, packageName, version, path) {
|
|
424
|
+
const pattern = new RegExp(`(\\[\\[package\\]\\]\\nname = "${escapeRegExp(packageName)}"\\nversion = ")[^"]+(")`, "u");
|
|
425
|
+
const nextSource = source.replace(pattern, `$1${version}$2`);
|
|
426
|
+
if (nextSource === source) {
|
|
427
|
+
throw new Error(`${path} is missing a package lock entry for ${packageName}.`);
|
|
428
|
+
}
|
|
429
|
+
return nextSource;
|
|
430
|
+
}
|
|
431
|
+
async function currentGitRef() {
|
|
432
|
+
const { stdout: branch } = await nanoSpawn("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
433
|
+
stdin: "ignore",
|
|
434
|
+
});
|
|
435
|
+
const trimmedBranch = branch.trim();
|
|
436
|
+
if (trimmedBranch !== "HEAD") {
|
|
437
|
+
return trimmedBranch;
|
|
438
|
+
}
|
|
439
|
+
const { stdout: commit } = await nanoSpawn("git", ["rev-parse", "HEAD"], {
|
|
440
|
+
stdin: "ignore",
|
|
441
|
+
});
|
|
442
|
+
return commit.trim();
|
|
443
|
+
}
|
|
444
|
+
async function resetGeneratedDirectory(path) {
|
|
445
|
+
assertSafeGeneratedDirectory(path);
|
|
446
|
+
await rm(path, { force: true, recursive: true });
|
|
447
|
+
await mkdir(path, { recursive: true });
|
|
448
|
+
}
|
|
449
|
+
function assertSafeGeneratedDirectory(path) {
|
|
450
|
+
const resolvedPath = resolve(path);
|
|
451
|
+
const relativePath = relative(process.cwd(), resolvedPath).split(sep).join("/");
|
|
452
|
+
if (relativePath === "" ||
|
|
453
|
+
relativePath === "." ||
|
|
454
|
+
relativePath.startsWith("../") ||
|
|
455
|
+
relativePath === "tauri" ||
|
|
456
|
+
relativePath.startsWith("tauri/")) {
|
|
457
|
+
throw new Error(`Refusing to reset unsafe Tauri release directory: ${path}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function formatReleasePath(template, version) {
|
|
461
|
+
return template.replaceAll("${version}", version);
|
|
462
|
+
}
|
|
463
|
+
function joinPublicUrl(baseUrl, remotePath) {
|
|
464
|
+
const url = new URL(baseUrl);
|
|
465
|
+
const basePath = url.pathname.replace(/\/+$/u, "");
|
|
466
|
+
const nextPath = joinRemotePath(basePath, remotePath);
|
|
467
|
+
url.pathname = nextPath.startsWith("/") ? nextPath : `/${nextPath}`;
|
|
468
|
+
return url.toString();
|
|
469
|
+
}
|
|
470
|
+
function joinRemotePath(...parts) {
|
|
471
|
+
return parts
|
|
472
|
+
.flatMap((part) => part.split("/"))
|
|
473
|
+
.map((part) => part.trim())
|
|
474
|
+
.filter(Boolean)
|
|
475
|
+
.join("/");
|
|
476
|
+
}
|
|
477
|
+
function parsePositiveInteger(value, name) {
|
|
478
|
+
if (!value) {
|
|
479
|
+
return 0;
|
|
480
|
+
}
|
|
481
|
+
const parsed = Number(value);
|
|
482
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
483
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
484
|
+
}
|
|
485
|
+
return parsed;
|
|
486
|
+
}
|
|
487
|
+
function uniquePlatforms(platforms) {
|
|
488
|
+
return [...new Set(platforms)];
|
|
489
|
+
}
|
|
490
|
+
function isTauriReleasePlatform(value) {
|
|
491
|
+
return tauriReleasePlatforms.includes(value);
|
|
492
|
+
}
|
|
493
|
+
async function pathExists(path) {
|
|
494
|
+
try {
|
|
495
|
+
await access(path, constants.F_OK);
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function isFile(path) {
|
|
503
|
+
try {
|
|
504
|
+
return (await stat(path)).isFile();
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function escapeRegExp(value) {
|
|
511
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
512
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type PackageJson = {
|
|
2
|
+
name?: string;
|
|
3
|
+
version?: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
};
|
|
6
|
+
type ReleasePackageJson = PackageJson & {
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function nextReleaseVersion(currentVersion: string, now?: Date): string;
|
|
11
|
+
export declare function releaseName(packageName: string, version: string): string;
|
|
12
|
+
export declare function formatReleaseDate(date: Date): string;
|
|
13
|
+
export declare function readPackageJson(path?: string): Promise<{
|
|
14
|
+
packageJson: ReleasePackageJson;
|
|
15
|
+
source: string;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function writePackageVersion(path: string, packageJson: PackageJson, version: string): Promise<void>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
export function nextReleaseVersion(currentVersion, now = new Date()) {
|
|
3
|
+
const current = parseVersion(currentVersion);
|
|
4
|
+
const releaseDate = formatReleaseDate(now);
|
|
5
|
+
const patch = current.minor === releaseDate ? current.patch + 1 : 0;
|
|
6
|
+
return `${current.major}.${releaseDate}.${patch}`;
|
|
7
|
+
}
|
|
8
|
+
export function releaseName(packageName, version) {
|
|
9
|
+
return `${packageName}@${version}`;
|
|
10
|
+
}
|
|
11
|
+
export function formatReleaseDate(date) {
|
|
12
|
+
const year = String(date.getFullYear());
|
|
13
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
14
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
15
|
+
return `${year}${month}${day}`;
|
|
16
|
+
}
|
|
17
|
+
export async function readPackageJson(path = "package.json") {
|
|
18
|
+
const source = await readFile(path, "utf8");
|
|
19
|
+
const packageJson = JSON.parse(source);
|
|
20
|
+
if (!packageJson.name) {
|
|
21
|
+
throw new Error(`${path} is missing a package name.`);
|
|
22
|
+
}
|
|
23
|
+
if (!packageJson.version) {
|
|
24
|
+
throw new Error(`${path} is missing a package version.`);
|
|
25
|
+
}
|
|
26
|
+
return { packageJson: packageJson, source };
|
|
27
|
+
}
|
|
28
|
+
export async function writePackageVersion(path, packageJson, version) {
|
|
29
|
+
await writeFile(path, `${JSON.stringify({ ...packageJson, version }, null, 2)}\n`);
|
|
30
|
+
}
|
|
31
|
+
function parseVersion(version) {
|
|
32
|
+
const match = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)$/u.exec(version);
|
|
33
|
+
if (!match?.groups) {
|
|
34
|
+
throw new Error(`Invalid package version "${version}". Expected major.minor.patch.`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
major: Number(match.groups.major),
|
|
38
|
+
minor: match.groups.minor,
|
|
39
|
+
patch: Number(match.groups.patch),
|
|
40
|
+
};
|
|
41
|
+
}
|
package/dist/deploy.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import type { CoreframeConfig } from "./config.ts";
|
|
3
|
+
import type { TauriReleasePlatform } from "./deploy/config.ts";
|
|
4
|
+
type DeployArgs = {
|
|
5
|
+
runMigrations: boolean;
|
|
6
|
+
skipMigrations: boolean;
|
|
7
|
+
skipPostDeployChecks: boolean;
|
|
8
|
+
tauriLocalPlatforms: TauriReleasePlatform[];
|
|
9
|
+
tauriWorkflowName?: string;
|
|
10
|
+
tauriWorkflowPlatforms: TauriReleasePlatform[];
|
|
11
|
+
tauriWorkflowRef?: string;
|
|
12
|
+
yes: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function deployProject(argv?: string[], env?: NodeJS.ProcessEnv): Promise<void>;
|
|
15
|
+
export declare function readProjectConfig(): Promise<CoreframeConfig>;
|
|
16
|
+
export declare function parseDeployArgs(argv: string[]): DeployArgs;
|
|
17
|
+
export {};
|