Skip to content

Commit

Permalink
feat: Add CLI and API for only publishing assets (skipping build fl…
Browse files Browse the repository at this point in the history
…ow) (#8150)
  • Loading branch information
mmaietta committed Apr 3, 2024
1 parent 15bffa0 commit f4e6ae2
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 19 deletions.
6 changes: 6 additions & 0 deletions .changeset/flat-keys-wonder.md
@@ -0,0 +1,6 @@
---
"app-builder-lib": minor
"electron-builder": minor
---

feat: add functionality to just publish artifacts
44 changes: 44 additions & 0 deletions docs/api/electron-builder.md
Expand Up @@ -22,6 +22,7 @@ Developer API only. See [Configuration](../configuration/configuration.md) for u
<li><a href="#Arch"><code>.Arch</code></a> : <code>enum</code></li>
<li><a href="#module_electron-builder.build"><code>.build(rawOptions)</code></a> ⇒ <code>Promise&lt;Array&lt;String&gt;&gt;</code></li>
<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>
<li><a href="#module_electron-builder.publish"><code>.publish(args)</code></a> ⇒ <code>Promise&lt;void&gt;</code></li>
</ul>
</li>
</ul>
Expand Down Expand Up @@ -78,6 +79,23 @@ Developer API only. See [Configuration](../configuration/configuration.md) for u
</tr>
</tbody>
</table>
<p><a name="module_electron-builder.publish"></a></p>
<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>
<p><strong>Kind</strong>: method of <a href="#module_electron-builder"><code>electron-builder</code></a><br/></p>
<table>
<thead>
<tr>
<th>Param</th>
<th>Type</th>
</tr>
</thead>
<tbody>
<tr>
<td>args</td>
<td><code>Object&lt;String, any&gt;</code></td>
</tr>
</tbody>
</table>
<p><a name="module_app-builder-lib"></a></p>
<h1 id="app-builder-lib">app-builder-lib</h1>
<ul>
Expand Down Expand Up @@ -206,6 +224,7 @@ Developer API only. See [Configuration](../configuration/configuration.md) for u
<li><a href="#module_app-builder-lib.PublishManager+awaitTasks"><code>.awaitTasks()</code></a> ⇒ <code>Promise&lt;void&gt;</code></li>
<li><a href="#module_app-builder-lib.PublishManager+cancelTasks"><code>.cancelTasks()</code></a></li>
<li><a href="#module_app-builder-lib.PublishManager+getGlobalPublishConfigurations"><code>.getGlobalPublishConfigurations()</code></a> ⇒ <code>Promise&lt; | Array&gt;</code></li>
<li><a href="#module_app-builder-lib.PublishManager+scheduleUpload"><code>.scheduleUpload(publishConfig, event, appInfo)</code></a></li>
</ul>
</li>
<li><a href="#Target">.Target</a>
Expand Down Expand Up @@ -2130,6 +2149,7 @@ return path.join(target.outDir, <code>__${target.name}-${getArtifactArchName(arc
<li><a href="#module_app-builder-lib.PublishManager+awaitTasks"><code>.awaitTasks()</code></a> ⇒ <code>Promise&lt;void&gt;</code></li>
<li><a href="#module_app-builder-lib.PublishManager+cancelTasks"><code>.cancelTasks()</code></a></li>
<li><a href="#module_app-builder-lib.PublishManager+getGlobalPublishConfigurations"><code>.getGlobalPublishConfigurations()</code></a> ⇒ <code>Promise&lt; | Array&gt;</code></li>
<li><a href="#module_app-builder-lib.PublishManager+scheduleUpload"><code>.scheduleUpload(publishConfig, event, appInfo)</code></a></li>
</ul>
</li>
</ul>
Expand All @@ -2139,6 +2159,30 @@ return path.join(target.outDir, <code>__${target.name}-${getArtifactArchName(arc
<h3 id="publishmanager.canceltasks()"><code>publishManager.cancelTasks()</code></h3>
<p><a name="module_app-builder-lib.PublishManager+getGlobalPublishConfigurations"></a></p>
<h3 id="publishmanager.getglobalpublishconfigurations()-%E2%87%92-promise%3C-%7C-array%3E"><code>publishManager.getGlobalPublishConfigurations()</code> ⇒ <code>Promise&lt; | Array&gt;</code></h3>
<p><a name="module_app-builder-lib.PublishManager+scheduleUpload"></a></p>
<h3 id="publishmanager.scheduleupload(publishconfig%2C-event%2C-appinfo)"><code>publishManager.scheduleUpload(publishConfig, event, appInfo)</code></h3>
<table>
<thead>
<tr>
<th>Param</th>
<th>Type</th>
</tr>
</thead>
<tbody>
<tr>
<td>publishConfig</td>
<td><code><a href="/configuration/publish#publishconfiguration">PublishConfiguration</a></code></td>
</tr>
<tr>
<td>event</td>
<td><code>module:packages/electron-publish/out/publisher.UploadTask</code></td>
</tr>
<tr>
<td>appInfo</td>
<td><code><a href="#AppInfo">AppInfo</a></code></td>
</tr>
</tbody>
</table>
<p><a name="Target"></a></p>
<h2 id="target">Target</h2>
<p><strong>Kind</strong>: class of <a href="#module_app-builder-lib"><code>app-builder-lib</code></a><br/>
Expand Down
24 changes: 12 additions & 12 deletions packages/app-builder-lib/src/packager.ts
Expand Up @@ -19,7 +19,7 @@ import { ArtifactBuildStarted, ArtifactCreated, PackagerOptions } from "./packag
import { PlatformPackager, resolveFunction } from "./platformPackager"
import { ProtonFramework } from "./ProtonFramework"
import { computeArchToTargetNamesMap, createTargets, NoOpTarget } from "./targets/targetFactory"
import { computeDefaultAppDirectory, getConfig, validateConfig } from "./util/config"
import { computeDefaultAppDirectory, getConfig, validateConfiguration } from "./util/config"
import { expandMacro } from "./util/macroExpander"
import { createLazyProductionDeps, NodeModuleDirInfo } from "./util/packageDependencies"
import { checkMetadata, readPackageJson } from "./util/packageMetadata"
Expand Down Expand Up @@ -296,7 +296,7 @@ export class Packager {
}
}

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

return await this._build(configuration, this._metadata, this._devMetadata)
}
await validateConfiguration(configuration, this.debugLogger)

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

// external caller of this method always uses isTwoPackageJsonProjectLayoutUsed=false and appDir=projectDir, no way (and need) to use another values
async build(repositoryInfo?: SourceRepositoryInfo): Promise<BuildResult> {
await this.validateConfig()

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

const commonOutDirWithoutPossibleOsMacro = path.resolve(
this.projectDir,
expandMacro(configuration.directories!.output!, null, this._appInfo, {
expandMacro(this.config.directories!.output!, null, this._appInfo, {
os: "",
})
)

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

// because artifact event maybe dispatched several times for different publish providers
Expand Down Expand Up @@ -394,7 +394,7 @@ export class Packager {
outDir: commonOutDirWithoutPossibleOsMacro,
artifactPaths: Array.from(artifactPaths),
platformToTargets,
configuration,
configuration: this.config,
}
}

Expand Down Expand Up @@ -439,7 +439,7 @@ export class Packager {
}

// support os and arch macro in output value
const outDir = path.resolve(this.projectDir, packager.expandMacro(this._configuration!.directories!.output!, Arch[arch]))
const outDir = path.resolve(this.projectDir, packager.expandMacro(this.config.directories!.output!, Arch[arch]))
const targetList = createTargets(nameToTarget, targetNames.length === 0 ? packager.defaultTarget : targetNames, outDir, packager)
await createOutDirIfNeed(targetList, createdOutDirs)
await packager.pack(outDir, arch, targetList, taskManager)
Expand Down
1 change: 0 additions & 1 deletion packages/app-builder-lib/src/publish/PublishManager.ts
Expand Up @@ -143,7 +143,6 @@ export class PublishManager implements PublishContext {
return await resolvePublishConfigurations(publishers, null, this.packager, null, true)
}

/** @internal */
scheduleUpload(publishConfig: PublishConfiguration, event: UploadTask, appInfo: AppInfo): void {
if (publishConfig.provider === "generic") {
return
Expand Down
2 changes: 1 addition & 1 deletion packages/app-builder-lib/src/util/config.ts
Expand Up @@ -217,7 +217,7 @@ function getDefaultConfig(): Configuration {

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

export async function validateConfig(config: Configuration, debugLogger: DebugLogger) {
export async function validateConfiguration(config: Configuration, debugLogger: DebugLogger) {
const extraMetadata = config.extraMetadata
if (extraMetadata != null) {
if (extraMetadata.build != null) {
Expand Down
2 changes: 2 additions & 0 deletions packages/electron-builder/src/cli/cli.ts
Expand Up @@ -13,12 +13,14 @@ import { configureInstallAppDepsCommand, installAppDeps } from "./install-app-de
import { start } from "./start"
import { nodeGypRebuild } from "app-builder-lib/out/util/yarn"
import { getElectronVersion } from "app-builder-lib/out/electron/electronVersion"
import { configurePublishCommand, publish } from "../publish"

// tslint:disable:no-unused-expression
void createYargs()
.command(["build", "*"], "Build", configureBuildCommand, wrap(build))
.command("install-app-deps", "Install app deps", configureInstallAppDepsCommand, wrap(installAppDeps))
.command("node-gyp-rebuild", "Rebuild own native code", configureInstallAppDepsCommand /* yes, args the same as for install app deps */, wrap(rebuildAppNativeCode))
.command("publish", "Publish a list of artifacts", configurePublishCommand, wrap(publish))
.command(
"create-self-signed-cert",
"Create self-signed code signing cert for Windows apps",
Expand Down
1 change: 1 addition & 0 deletions packages/electron-builder/src/index.ts
@@ -1,5 +1,6 @@
export { getArchSuffix, Arch, archFromString, log } from "builder-util"
export { build, CliOptions, createTargets } from "./builder"
export { publish, publishArtifactsWithOptions } from "./publish"
export {
TargetConfiguration,
Platform,
Expand Down
122 changes: 122 additions & 0 deletions packages/electron-builder/src/publish.ts
@@ -0,0 +1,122 @@
#! /usr/bin/env node

import { AppInfo, CancellationToken, Packager, PackagerOptions, PublishManager, PublishOptions, UploadTask, checkBuildRequestOptions } from "app-builder-lib"
import { Publish } from "app-builder-lib/out/core"
import { computeSafeArtifactNameIfNeeded } from "app-builder-lib/out/platformPackager"
import { getConfig } from "app-builder-lib/out/util/config"
import { InvalidConfigurationError, archFromString, log } from "builder-util"
import { printErrorAndExit } from "builder-util/out/promise"
import * as chalk from "chalk"
import * as path from "path"
import * as yargs from "yargs"
import { BuildOptions, normalizeOptions } from "./builder"

/** @internal */
export function configurePublishCommand(yargs: yargs.Argv): yargs.Argv {
// https://github.com/yargs/yargs/issues/760
// demandOption is required to be set
return yargs
.parserConfiguration({
"camel-case-expansion": false,
})
.option("files", {
alias: "f",
string: true,
type: "array",
requiresArg: true,
description: "The file(s) to upload to your publisher",
})
.option("version", {
alias: ["v"],
type: "string",
description: "The app/build version used when searching for an upload release (used by some Publishers)",
})
.option("config", {
alias: ["c"],
type: "string",
description:
"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"),
})
.demandOption("files")
}

export async function publish(args: { files: string[]; version: string | undefined; config: string | undefined }) {
const uploadTasks = args.files.map(f => {
return {
file: path.resolve(f),
arch: null,
}
})
return publishArtifactsWithOptions(uploadTasks, args.version, args.config)
}

export async function publishArtifactsWithOptions(
uploadOptions: { file: string; arch: string | null }[],
buildVersion?: string,
configurationFilePath?: string,
publishConfiguration?: Publish
) {
const projectDir = process.cwd()
const config = await getConfig(projectDir, configurationFilePath || null, { publish: publishConfiguration, detectUpdateChannel: false })

const buildOptions: BuildOptions = normalizeOptions({ config })
checkBuildRequestOptions(buildOptions)

const uniqueUploads = Array.from(new Set(uploadOptions))
const tasks: UploadTask[] = uniqueUploads.map(({ file, arch }) => {
const filename = path.basename(file)
return { file, arch: arch ? archFromString(arch) : null, safeArtifactName: computeSafeArtifactNameIfNeeded(filename, () => filename) }
})

return publishPackageWithTasks(buildOptions, tasks, buildVersion)
}

async function publishPackageWithTasks(
options: PackagerOptions & PublishOptions,
uploadTasks: UploadTask[],
buildVersion?: string,
cancellationToken: CancellationToken = new CancellationToken(),
packager: Packager = new Packager(options, cancellationToken)
) {
await packager.validateConfig()
const appInfo = new AppInfo(packager, buildVersion)
const publishManager = new PublishManager(packager, options, cancellationToken)

const sigIntHandler = () => {
log.warn("cancelled by SIGINT")
packager.cancellationToken.cancel()
publishManager.cancelTasks()
}
process.once("SIGINT", sigIntHandler)

try {
const publishConfigurations = await publishManager.getGlobalPublishConfigurations()
if (publishConfigurations == null || publishConfigurations.length === 0) {
throw new InvalidConfigurationError("unable to find any publish configuration")
}

for (const newArtifact of uploadTasks) {
for (const publishConfiguration of publishConfigurations) {
publishManager.scheduleUpload(publishConfiguration, newArtifact, appInfo)
}
}

await publishManager.awaitTasks()
return uploadTasks
} catch (error: any) {
packager.cancellationToken.cancel()
publishManager.cancelTasks()
process.removeListener("SIGINT", sigIntHandler)
log.error({ message: (error.stack || error.message || error).toString() }, "error publishing")
}
return null
}

function main() {
return publish(configurePublishCommand(yargs).argv as any)
}

if (require.main === module) {
log.warn("please use as subcommand: electron-builder publish")
main().catch(printErrorAndExit)
}
11 changes: 9 additions & 2 deletions test/src/ArtifactPublisherTest.ts
Expand Up @@ -8,6 +8,7 @@ import { KeygenPublisher } from "app-builder-lib/out/publish/KeygenPublisher"
import { Platform } from "app-builder-lib"
import { createPublisher } from "app-builder-lib/out/publish/PublishManager"
import { BitbucketPublisher } from "app-builder-lib/out/publish/BitbucketPublisher"
import { publishArtifactsWithOptions } from "electron-builder"

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

test.ifEnv(process.env.BITBUCKET_TOKEN)("Bitbucket upload", async () => {
const timeout = 0
const publisher = new BitbucketPublisher(publishContext, {
const config: BitbucketOptions = {
provider: "bitbucket",
owner: "mike-m",
slug: "electron-builder-test",
timeout,
} as BitbucketOptions)
}
const publisher = new BitbucketPublisher(publishContext, config)
const filename = await publisher.upload({ file: iconPath, arch: Arch.x64, timeout })
await publisher.deleteRelease(filename)

const uploadTasks: any = await publishArtifactsWithOptions([{ file: icoPath, arch: null }], undefined, undefined, [config])
for (const task of uploadTasks) {
await publisher.deleteRelease(task.file)
}
})

test.ifEnv(process.env.BITBUCKET_TOKEN)("Bitbucket upload", async () => {
Expand Down
6 changes: 3 additions & 3 deletions test/src/configurationValidationTest.ts
@@ -1,6 +1,6 @@
import { DebugLogger } from "builder-util/out/DebugLogger"
import { Configuration, Platform } from "electron-builder"
import { validateConfig } from "app-builder-lib/out/util/config"
import { validateConfiguration } from "app-builder-lib/out/util/config"
import { createYargs, configureBuildCommand, normalizeOptions, CliOptions } from "electron-builder/out/builder"
import { app, appThrows, linuxDirTarget } from "./helpers/packTester"

Expand Down Expand Up @@ -64,7 +64,7 @@ test.ifAll.ifDevOrLinuxCi(
)

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

0 comments on commit f4e6ae2

Please sign in to comment.