Skip to content

Commit f4e6ae2

Browse files
authoredApr 3, 2024··
feat: Add CLI and API for only publishing assets (skipping build flow) (#8150)
1 parent 15bffa0 commit f4e6ae2

File tree

10 files changed

+200
-19
lines changed

10 files changed

+200
-19
lines changed
 

‎.changeset/flat-keys-wonder.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"app-builder-lib": minor
3+
"electron-builder": minor
4+
---
5+
6+
feat: add functionality to just publish artifacts

‎docs/api/electron-builder.md

+44
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Developer API only. See [Configuration](../configuration/configuration.md) for u
2222
<li><a href="#Arch"><code>.Arch</code></a> : <code>enum</code></li>
2323
<li><a href="#module_electron-builder.build"><code>.build(rawOptions)</code></a> ⇒ <code>Promise&lt;Array&lt;String&gt;&gt;</code></li>
2424
<li><a href="#module_electron-builder.createTargets"><code>.createTargets(platforms, type, arch)</code></a> ⇒ <code>Map&lt;Platform | Map&lt;<a href="#Arch">Arch</a> | Array&lt;String&gt;&gt;&gt;</code></li>
25+
<li><a href="#module_electron-builder.publish"><code>.publish(args)</code></a> ⇒ <code>Promise&lt;void&gt;</code></li>
2526
</ul>
2627
</li>
2728
</ul>
@@ -78,6 +79,23 @@ Developer API only. See [Configuration](../configuration/configuration.md) for u
7879
</tr>
7980
</tbody>
8081
</table>
82+
<p><a name="module_electron-builder.publish"></a></p>
83+
<h2 id="electron-builder.publish(args)-%E2%87%92-promise%3Cvoid%3E"><code>electron-builder.publish(args)</code> ⇒ <code>Promise&lt;void&gt;</code></h2>
84+
<p><strong>Kind</strong>: method of <a href="#module_electron-builder"><code>electron-builder</code></a><br/></p>
85+
<table>
86+
<thead>
87+
<tr>
88+
<th>Param</th>
89+
<th>Type</th>
90+
</tr>
91+
</thead>
92+
<tbody>
93+
<tr>
94+
<td>args</td>
95+
<td><code>Object&lt;String, any&gt;</code></td>
96+
</tr>
97+
</tbody>
98+
</table>
8199
<p><a name="module_app-builder-lib"></a></p>
82100
<h1 id="app-builder-lib">app-builder-lib</h1>
83101
<ul>
@@ -206,6 +224,7 @@ Developer API only. See [Configuration](../configuration/configuration.md) for u
206224
<li><a href="#module_app-builder-lib.PublishManager+awaitTasks"><code>.awaitTasks()</code></a> ⇒ <code>Promise&lt;void&gt;</code></li>
207225
<li><a href="#module_app-builder-lib.PublishManager+cancelTasks"><code>.cancelTasks()</code></a></li>
208226
<li><a href="#module_app-builder-lib.PublishManager+getGlobalPublishConfigurations"><code>.getGlobalPublishConfigurations()</code></a> ⇒ <code>Promise&lt; | Array&gt;</code></li>
227+
<li><a href="#module_app-builder-lib.PublishManager+scheduleUpload"><code>.scheduleUpload(publishConfig, event, appInfo)</code></a></li>
209228
</ul>
210229
</li>
211230
<li><a href="#Target">.Target</a>
@@ -2130,6 +2149,7 @@ return path.join(target.outDir, <code>__${target.name}-${getArtifactArchName(arc
21302149
<li><a href="#module_app-builder-lib.PublishManager+awaitTasks"><code>.awaitTasks()</code></a> ⇒ <code>Promise&lt;void&gt;</code></li>
21312150
<li><a href="#module_app-builder-lib.PublishManager+cancelTasks"><code>.cancelTasks()</code></a></li>
21322151
<li><a href="#module_app-builder-lib.PublishManager+getGlobalPublishConfigurations"><code>.getGlobalPublishConfigurations()</code></a> ⇒ <code>Promise&lt; | Array&gt;</code></li>
2152+
<li><a href="#module_app-builder-lib.PublishManager+scheduleUpload"><code>.scheduleUpload(publishConfig, event, appInfo)</code></a></li>
21332153
</ul>
21342154
</li>
21352155
</ul>
@@ -2139,6 +2159,30 @@ return path.join(target.outDir, <code>__${target.name}-${getArtifactArchName(arc
21392159
<h3 id="publishmanager.canceltasks()"><code>publishManager.cancelTasks()</code></h3>
21402160
<p><a name="module_app-builder-lib.PublishManager+getGlobalPublishConfigurations"></a></p>
21412161
<h3 id="publishmanager.getglobalpublishconfigurations()-%E2%87%92-promise%3C-%7C-array%3E"><code>publishManager.getGlobalPublishConfigurations()</code> ⇒ <code>Promise&lt; | Array&gt;</code></h3>
2162+
<p><a name="module_app-builder-lib.PublishManager+scheduleUpload"></a></p>
2163+
<h3 id="publishmanager.scheduleupload(publishconfig%2C-event%2C-appinfo)"><code>publishManager.scheduleUpload(publishConfig, event, appInfo)</code></h3>
2164+
<table>
2165+
<thead>
2166+
<tr>
2167+
<th>Param</th>
2168+
<th>Type</th>
2169+
</tr>
2170+
</thead>
2171+
<tbody>
2172+
<tr>
2173+
<td>publishConfig</td>
2174+
<td><code><a href="/configuration/publish#publishconfiguration">PublishConfiguration</a></code></td>
2175+
</tr>
2176+
<tr>
2177+
<td>event</td>
2178+
<td><code>module:packages/electron-publish/out/publisher.UploadTask</code></td>
2179+
</tr>
2180+
<tr>
2181+
<td>appInfo</td>
2182+
<td><code><a href="#AppInfo">AppInfo</a></code></td>
2183+
</tr>
2184+
</tbody>
2185+
</table>
21422186
<p><a name="Target"></a></p>
21432187
<h2 id="target">Target</h2>
21442188
<p><strong>Kind</strong>: class of <a href="#module_app-builder-lib"><code>app-builder-lib</code></a><br/>

‎packages/app-builder-lib/src/packager.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { ArtifactBuildStarted, ArtifactCreated, PackagerOptions } from "./packag
1919
import { PlatformPackager, resolveFunction } from "./platformPackager"
2020
import { ProtonFramework } from "./ProtonFramework"
2121
import { computeArchToTargetNamesMap, createTargets, NoOpTarget } from "./targets/targetFactory"
22-
import { computeDefaultAppDirectory, getConfig, validateConfig } from "./util/config"
22+
import { computeDefaultAppDirectory, getConfig, validateConfiguration } from "./util/config"
2323
import { expandMacro } from "./util/macroExpander"
2424
import { createLazyProductionDeps, NodeModuleDirInfo } from "./util/packageDependencies"
2525
import { checkMetadata, readPackageJson } from "./util/packageMetadata"
@@ -296,7 +296,7 @@ export class Packager {
296296
}
297297
}
298298

299-
async build(): Promise<BuildResult> {
299+
async validateConfig(): Promise<void> {
300300
let configPath: string | null = null
301301
let configFromOptions = this.options.config
302302
if (typeof configFromOptions === "string") {
@@ -337,15 +337,15 @@ export class Packager {
337337
}
338338
checkMetadata(this.metadata, this.devMetadata, appPackageFile, devPackageFile)
339339

340-
return await this._build(configuration, this._metadata, this._devMetadata)
341-
}
340+
await validateConfiguration(configuration, this.debugLogger)
342341

343-
// external caller of this method always uses isTwoPackageJsonProjectLayoutUsed=false and appDir=projectDir, no way (and need) to use another values
344-
async _build(configuration: Configuration, metadata: Metadata, devMetadata: Metadata | null, repositoryInfo?: SourceRepositoryInfo): Promise<BuildResult> {
345-
await validateConfig(configuration, this.debugLogger)
346342
this._configuration = configuration
347-
this._metadata = metadata
348343
this._devMetadata = devMetadata
344+
}
345+
346+
// external caller of this method always uses isTwoPackageJsonProjectLayoutUsed=false and appDir=projectDir, no way (and need) to use another values
347+
async build(repositoryInfo?: SourceRepositoryInfo): Promise<BuildResult> {
348+
await this.validateConfig()
349349

350350
if (repositoryInfo != null) {
351351
this._repositoryInfo.value = Promise.resolve(repositoryInfo)
@@ -356,15 +356,15 @@ export class Packager {
356356

357357
const commonOutDirWithoutPossibleOsMacro = path.resolve(
358358
this.projectDir,
359-
expandMacro(configuration.directories!.output!, null, this._appInfo, {
359+
expandMacro(this.config.directories!.output!, null, this._appInfo, {
360360
os: "",
361361
})
362362
)
363363

364364
if (!isCI && (process.stdout as any).isTTY) {
365365
const effectiveConfigFile = path.join(commonOutDirWithoutPossibleOsMacro, "builder-effective-config.yaml")
366366
log.info({ file: log.filePath(effectiveConfigFile) }, "writing effective config")
367-
await outputFile(effectiveConfigFile, getSafeEffectiveConfig(configuration))
367+
await outputFile(effectiveConfigFile, getSafeEffectiveConfig(this.config))
368368
}
369369

370370
// because artifact event maybe dispatched several times for different publish providers
@@ -394,7 +394,7 @@ export class Packager {
394394
outDir: commonOutDirWithoutPossibleOsMacro,
395395
artifactPaths: Array.from(artifactPaths),
396396
platformToTargets,
397-
configuration,
397+
configuration: this.config,
398398
}
399399
}
400400

@@ -439,7 +439,7 @@ export class Packager {
439439
}
440440

441441
// support os and arch macro in output value
442-
const outDir = path.resolve(this.projectDir, packager.expandMacro(this._configuration!.directories!.output!, Arch[arch]))
442+
const outDir = path.resolve(this.projectDir, packager.expandMacro(this.config.directories!.output!, Arch[arch]))
443443
const targetList = createTargets(nameToTarget, targetNames.length === 0 ? packager.defaultTarget : targetNames, outDir, packager)
444444
await createOutDirIfNeed(targetList, createdOutDirs)
445445
await packager.pack(outDir, arch, targetList, taskManager)

‎packages/app-builder-lib/src/publish/PublishManager.ts

-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ export class PublishManager implements PublishContext {
143143
return await resolvePublishConfigurations(publishers, null, this.packager, null, true)
144144
}
145145

146-
/** @internal */
147146
scheduleUpload(publishConfig: PublishConfiguration, event: UploadTask, appInfo: AppInfo): void {
148147
if (publishConfig.provider === "generic") {
149148
return

‎packages/app-builder-lib/src/util/config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ function getDefaultConfig(): Configuration {
217217

218218
const schemeDataPromise = new Lazy(() => readJson(path.join(__dirname, "..", "..", "scheme.json")))
219219

220-
export async function validateConfig(config: Configuration, debugLogger: DebugLogger) {
220+
export async function validateConfiguration(config: Configuration, debugLogger: DebugLogger) {
221221
const extraMetadata = config.extraMetadata
222222
if (extraMetadata != null) {
223223
if (extraMetadata.build != null) {

‎packages/electron-builder/src/cli/cli.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import { configureInstallAppDepsCommand, installAppDeps } from "./install-app-de
1313
import { start } from "./start"
1414
import { nodeGypRebuild } from "app-builder-lib/out/util/yarn"
1515
import { getElectronVersion } from "app-builder-lib/out/electron/electronVersion"
16+
import { configurePublishCommand, publish } from "../publish"
1617

1718
// tslint:disable:no-unused-expression
1819
void createYargs()
1920
.command(["build", "*"], "Build", configureBuildCommand, wrap(build))
2021
.command("install-app-deps", "Install app deps", configureInstallAppDepsCommand, wrap(installAppDeps))
2122
.command("node-gyp-rebuild", "Rebuild own native code", configureInstallAppDepsCommand /* yes, args the same as for install app deps */, wrap(rebuildAppNativeCode))
23+
.command("publish", "Publish a list of artifacts", configurePublishCommand, wrap(publish))
2224
.command(
2325
"create-self-signed-cert",
2426
"Create self-signed code signing cert for Windows apps",

‎packages/electron-builder/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { getArchSuffix, Arch, archFromString, log } from "builder-util"
22
export { build, CliOptions, createTargets } from "./builder"
3+
export { publish, publishArtifactsWithOptions } from "./publish"
34
export {
45
TargetConfiguration,
56
Platform,
+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#! /usr/bin/env node
2+
3+
import { AppInfo, CancellationToken, Packager, PackagerOptions, PublishManager, PublishOptions, UploadTask, checkBuildRequestOptions } from "app-builder-lib"
4+
import { Publish } from "app-builder-lib/out/core"
5+
import { computeSafeArtifactNameIfNeeded } from "app-builder-lib/out/platformPackager"
6+
import { getConfig } from "app-builder-lib/out/util/config"
7+
import { InvalidConfigurationError, archFromString, log } from "builder-util"
8+
import { printErrorAndExit } from "builder-util/out/promise"
9+
import * as chalk from "chalk"
10+
import * as path from "path"
11+
import * as yargs from "yargs"
12+
import { BuildOptions, normalizeOptions } from "./builder"
13+
14+
/** @internal */
15+
export function configurePublishCommand(yargs: yargs.Argv): yargs.Argv {
16+
// https://github.com/yargs/yargs/issues/760
17+
// demandOption is required to be set
18+
return yargs
19+
.parserConfiguration({
20+
"camel-case-expansion": false,
21+
})
22+
.option("files", {
23+
alias: "f",
24+
string: true,
25+
type: "array",
26+
requiresArg: true,
27+
description: "The file(s) to upload to your publisher",
28+
})
29+
.option("version", {
30+
alias: ["v"],
31+
type: "string",
32+
description: "The app/build version used when searching for an upload release (used by some Publishers)",
33+
})
34+
.option("config", {
35+
alias: ["c"],
36+
type: "string",
37+
description:
38+
"The path to an electron-builder config. Defaults to `electron-builder.yml` (or `json`, or `json5`, or `js`, or `ts`), see " + chalk.underline("https://goo.gl/YFRJOM"),
39+
})
40+
.demandOption("files")
41+
}
42+
43+
export async function publish(args: { files: string[]; version: string | undefined; config: string | undefined }) {
44+
const uploadTasks = args.files.map(f => {
45+
return {
46+
file: path.resolve(f),
47+
arch: null,
48+
}
49+
})
50+
return publishArtifactsWithOptions(uploadTasks, args.version, args.config)
51+
}
52+
53+
export async function publishArtifactsWithOptions(
54+
uploadOptions: { file: string; arch: string | null }[],
55+
buildVersion?: string,
56+
configurationFilePath?: string,
57+
publishConfiguration?: Publish
58+
) {
59+
const projectDir = process.cwd()
60+
const config = await getConfig(projectDir, configurationFilePath || null, { publish: publishConfiguration, detectUpdateChannel: false })
61+
62+
const buildOptions: BuildOptions = normalizeOptions({ config })
63+
checkBuildRequestOptions(buildOptions)
64+
65+
const uniqueUploads = Array.from(new Set(uploadOptions))
66+
const tasks: UploadTask[] = uniqueUploads.map(({ file, arch }) => {
67+
const filename = path.basename(file)
68+
return { file, arch: arch ? archFromString(arch) : null, safeArtifactName: computeSafeArtifactNameIfNeeded(filename, () => filename) }
69+
})
70+
71+
return publishPackageWithTasks(buildOptions, tasks, buildVersion)
72+
}
73+
74+
async function publishPackageWithTasks(
75+
options: PackagerOptions & PublishOptions,
76+
uploadTasks: UploadTask[],
77+
buildVersion?: string,
78+
cancellationToken: CancellationToken = new CancellationToken(),
79+
packager: Packager = new Packager(options, cancellationToken)
80+
) {
81+
await packager.validateConfig()
82+
const appInfo = new AppInfo(packager, buildVersion)
83+
const publishManager = new PublishManager(packager, options, cancellationToken)
84+
85+
const sigIntHandler = () => {
86+
log.warn("cancelled by SIGINT")
87+
packager.cancellationToken.cancel()
88+
publishManager.cancelTasks()
89+
}
90+
process.once("SIGINT", sigIntHandler)
91+
92+
try {
93+
const publishConfigurations = await publishManager.getGlobalPublishConfigurations()
94+
if (publishConfigurations == null || publishConfigurations.length === 0) {
95+
throw new InvalidConfigurationError("unable to find any publish configuration")
96+
}
97+
98+
for (const newArtifact of uploadTasks) {
99+
for (const publishConfiguration of publishConfigurations) {
100+
publishManager.scheduleUpload(publishConfiguration, newArtifact, appInfo)
101+
}
102+
}
103+
104+
await publishManager.awaitTasks()
105+
return uploadTasks
106+
} catch (error: any) {
107+
packager.cancellationToken.cancel()
108+
publishManager.cancelTasks()
109+
process.removeListener("SIGINT", sigIntHandler)
110+
log.error({ message: (error.stack || error.message || error).toString() }, "error publishing")
111+
}
112+
return null
113+
}
114+
115+
function main() {
116+
return publish(configurePublishCommand(yargs).argv as any)
117+
}
118+
119+
if (require.main === module) {
120+
log.warn("please use as subcommand: electron-builder publish")
121+
main().catch(printErrorAndExit)
122+
}

‎test/src/ArtifactPublisherTest.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { KeygenPublisher } from "app-builder-lib/out/publish/KeygenPublisher"
88
import { Platform } from "app-builder-lib"
99
import { createPublisher } from "app-builder-lib/out/publish/PublishManager"
1010
import { BitbucketPublisher } from "app-builder-lib/out/publish/BitbucketPublisher"
11+
import { publishArtifactsWithOptions } from "electron-builder"
1112

1213
if (isCi && process.platform === "win32") {
1314
fit("Skip ArtifactPublisherTest suite on Windows CI", () => {
@@ -150,14 +151,20 @@ test.ifEnv(process.env.KEYGEN_TOKEN)("Keygen upload", async () => {
150151

151152
test.ifEnv(process.env.BITBUCKET_TOKEN)("Bitbucket upload", async () => {
152153
const timeout = 0
153-
const publisher = new BitbucketPublisher(publishContext, {
154+
const config: BitbucketOptions = {
154155
provider: "bitbucket",
155156
owner: "mike-m",
156157
slug: "electron-builder-test",
157158
timeout,
158-
} as BitbucketOptions)
159+
}
160+
const publisher = new BitbucketPublisher(publishContext, config)
159161
const filename = await publisher.upload({ file: iconPath, arch: Arch.x64, timeout })
160162
await publisher.deleteRelease(filename)
163+
164+
const uploadTasks: any = await publishArtifactsWithOptions([{ file: icoPath, arch: null }], undefined, undefined, [config])
165+
for (const task of uploadTasks) {
166+
await publisher.deleteRelease(task.file)
167+
}
161168
})
162169

163170
test.ifEnv(process.env.BITBUCKET_TOKEN)("Bitbucket upload", async () => {

‎test/src/configurationValidationTest.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { DebugLogger } from "builder-util/out/DebugLogger"
22
import { Configuration, Platform } from "electron-builder"
3-
import { validateConfig } from "app-builder-lib/out/util/config"
3+
import { validateConfiguration } from "app-builder-lib/out/util/config"
44
import { createYargs, configureBuildCommand, normalizeOptions, CliOptions } from "electron-builder/out/builder"
55
import { app, appThrows, linuxDirTarget } from "./helpers/packTester"
66

@@ -64,7 +64,7 @@ test.ifAll.ifDevOrLinuxCi(
6464
)
6565

6666
test.ifAll.ifDevOrLinuxCi("files", () => {
67-
return validateConfig(
67+
return validateConfiguration(
6868
{
6969
appId: "com.example.myapp",
7070
files: [{ from: "dist/app", to: "app", filter: "*.js" }],
@@ -81,7 +81,7 @@ test.ifAll.ifDevOrLinuxCi("null string as null", async () => {
8181
const yargs = configureBuildCommand(createYargs())
8282
const options = normalizeOptions(yargs.parse(["-c.mac.identity=null", "--config.mac.hardenedRuntime=false"]) as CliOptions)
8383
const config = options.config as Configuration
84-
await validateConfig(config, new DebugLogger())
84+
await validateConfiguration(config, new DebugLogger())
8585
expect(config.mac!.identity).toBeNull()
8686
expect(config.mac!.hardenedRuntime).toBe(false)
8787
})

0 commit comments

Comments
 (0)
Please sign in to comment.