electron-cli 0.3.0-alpha.12 → 0.3.0-alpha.14

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.
@@ -8,20 +8,28 @@ use std::{
8
8
  use anyhow::{bail, Context, Result};
9
9
  use camino::Utf8PathBuf;
10
10
  use serde::{de::DeserializeOwned, Deserialize, Serialize};
11
+ use serde_json::{Map as JsonMap, Value as JsonValue};
11
12
 
12
13
  use crate::{
13
14
  cli::{MakeArgs, PublishArgs, PublishTarget},
14
15
  commands::make::{self, MakeReport},
15
16
  output,
17
+ project::ProjectSnapshot,
16
18
  };
17
19
 
18
20
  #[derive(Debug, Serialize)]
19
21
  struct PublishReport {
20
22
  make: MakeReport,
21
23
  publisher: String,
24
+ #[serde(skip)]
25
+ publisher_kind: PublishTarget,
22
26
  channel: String,
23
27
  local: Option<LocalPublishPlan>,
24
28
  github: Option<GithubPublishPlan>,
29
+ #[serde(skip)]
30
+ force_publish: bool,
31
+ #[serde(skip)]
32
+ github_auth_token: Option<String>,
25
33
  skip_make: bool,
26
34
  dry_run: bool,
27
35
  status: PublishStatus,
@@ -49,13 +57,21 @@ struct GithubPublishPlan {
49
57
  asset_url: Option<String>,
50
58
  }
51
59
 
52
- #[derive(Debug, Serialize)]
60
+ #[derive(Clone, Copy, Debug, Serialize)]
53
61
  #[serde(rename_all = "kebab-case")]
54
62
  enum PublishStatus {
55
63
  Planned,
56
64
  Published,
57
65
  }
58
66
 
67
+ #[derive(Debug, Serialize)]
68
+ struct PublishRunReport<'a> {
69
+ publishes: &'a [PublishReport],
70
+ dry_run: bool,
71
+ status: PublishStatus,
72
+ warnings: Vec<String>,
73
+ }
74
+
59
75
  #[derive(Debug, Serialize)]
60
76
  struct PublishManifest {
61
77
  schema_version: u8,
@@ -78,33 +94,112 @@ struct PublishedArtifact {
78
94
  size: u64,
79
95
  }
80
96
 
97
+ #[derive(Debug)]
98
+ struct ResolvedPublishers {
99
+ publishers: Vec<ResolvedPublisher>,
100
+ warnings: Vec<String>,
101
+ }
102
+
103
+ #[derive(Clone, Debug)]
104
+ struct ResolvedPublisher {
105
+ target: PublishTarget,
106
+ to: PathBuf,
107
+ channel: String,
108
+ github_repo: Option<String>,
109
+ github_tag: Option<String>,
110
+ github_tag_prefix: Option<String>,
111
+ github_release_name: Option<String>,
112
+ github_draft: bool,
113
+ github_prerelease: bool,
114
+ github_api_url: String,
115
+ github_auth_token: Option<String>,
116
+ force_publish: bool,
117
+ }
118
+
119
+ #[derive(Debug)]
120
+ struct ConfiguredPublisher {
121
+ label: String,
122
+ target: Option<PublishTarget>,
123
+ platforms: Vec<String>,
124
+ to: Option<PathBuf>,
125
+ channel: Option<String>,
126
+ github_repo: Option<String>,
127
+ github_tag: Option<String>,
128
+ github_tag_prefix: Option<String>,
129
+ github_release_name: Option<String>,
130
+ github_draft: Option<bool>,
131
+ github_prerelease: Option<bool>,
132
+ github_api_url: Option<String>,
133
+ github_auth_token: Option<String>,
134
+ force_publish: Option<bool>,
135
+ }
136
+
81
137
  pub fn run(args: PublishArgs) -> Result<()> {
82
- let mut report = build_report(&args)?;
138
+ let mut make_reports = make::build_reports(&make_args(&args))?;
139
+ let mut reports = build_reports_from_make_reports(&args, &make_reports)?;
83
140
 
84
141
  if args.dry_run {
85
- return print_report(&report, args.json);
142
+ return print_reports(&reports, args.json, PublishStatus::Planned);
86
143
  }
87
144
 
88
- execute_publish(&mut report, &args)?;
89
- report.status = PublishStatus::Published;
145
+ execute_publish_reports(&mut reports, &mut make_reports, &args)?;
90
146
 
91
- print_report(&report, args.json)
147
+ print_reports(&reports, args.json, PublishStatus::Published)
92
148
  }
93
149
 
150
+ #[cfg(test)]
94
151
  fn build_report(args: &PublishArgs) -> Result<PublishReport> {
95
- let make_args = MakeArgs {
96
- cwd: args.cwd.clone(),
97
- out_dir: args.out_dir.clone(),
98
- name: args.name.clone(),
99
- platform: args.platform.clone(),
100
- arch: args.arch.clone(),
101
- target: args.target,
102
- skip_package: false,
103
- force: args.force,
104
- dry_run: false,
105
- json: false,
106
- };
107
- let make = make::build_report(&make_args)?;
152
+ let reports = build_reports(args)?;
153
+ if reports.len() != 1 {
154
+ bail!(
155
+ "Expected one publish target, but resolved {}. Pass --target and --publisher to select one target.",
156
+ reports.len()
157
+ );
158
+ }
159
+ Ok(reports
160
+ .into_iter()
161
+ .next()
162
+ .expect("length was checked above"))
163
+ }
164
+
165
+ #[cfg(test)]
166
+ fn build_reports(args: &PublishArgs) -> Result<Vec<PublishReport>> {
167
+ let make_reports = make::build_reports(&make_args(args))?;
168
+ build_reports_from_make_reports(args, &make_reports)
169
+ }
170
+
171
+ fn build_reports_from_make_reports(
172
+ args: &PublishArgs,
173
+ make_reports: &[MakeReport],
174
+ ) -> Result<Vec<PublishReport>> {
175
+ let first_make = make_reports
176
+ .first()
177
+ .context("No make targets were resolved for publish.")?;
178
+ let project = first_make.package().project();
179
+ let platform = first_make.package().platform();
180
+ let resolved = resolve_publishers(project, args, platform)?;
181
+ let mut reports = Vec::new();
182
+
183
+ for publisher in &resolved.publishers {
184
+ for make in make_reports {
185
+ reports.push(build_report_for_publisher(
186
+ args,
187
+ make.clone(),
188
+ publisher,
189
+ &resolved.warnings,
190
+ )?);
191
+ }
192
+ }
193
+
194
+ Ok(reports)
195
+ }
196
+
197
+ fn build_report_for_publisher(
198
+ args: &PublishArgs,
199
+ make: MakeReport,
200
+ publisher: &ResolvedPublisher,
201
+ config_warnings: &[String],
202
+ ) -> Result<PublishReport> {
108
203
  let root = Path::new(make.package().project().root.as_str());
109
204
  let artifact_name = make
110
205
  .artifact()
@@ -113,22 +208,23 @@ fn build_report(args: &PublishArgs) -> Result<PublishReport> {
113
208
  .to_string();
114
209
 
115
210
  let mut warnings = make.warnings().to_vec();
211
+ warnings.extend(config_warnings.iter().cloned());
116
212
  if args.skip_make && !Path::new(make.artifact().as_str()).exists() {
117
213
  warnings.push(format!(
118
214
  "Make artifact does not exist: {}.",
119
215
  make.artifact()
120
216
  ));
121
217
  }
122
- let (local, github) = match args.publisher {
218
+ let (local, github) = match publisher.target {
123
219
  PublishTarget::Local => {
124
- let local = build_local_plan(root, args, &make, &artifact_name)?;
125
- if Path::new(local.destination_artifact.as_str()).exists() && !args.force {
220
+ let local = build_local_plan(root, publisher, &make, &artifact_name)?;
221
+ if Path::new(local.destination_artifact.as_str()).exists() && !publisher.force_publish {
126
222
  warnings.push(format!(
127
223
  "Publish artifact already exists: {}. Use --force to overwrite it.",
128
224
  local.destination_artifact
129
225
  ));
130
226
  }
131
- if Path::new(local.manifest.as_str()).exists() && !args.force {
227
+ if Path::new(local.manifest.as_str()).exists() && !publisher.force_publish {
132
228
  warnings.push(format!(
133
229
  "Publish manifest already exists: {}. Use --force to overwrite it.",
134
230
  local.manifest
@@ -137,17 +233,20 @@ fn build_report(args: &PublishArgs) -> Result<PublishReport> {
137
233
  (Some(local), None)
138
234
  }
139
235
  PublishTarget::Github => {
140
- let github = build_github_plan(args, &make, &artifact_name, &mut warnings)?;
236
+ let github = build_github_plan(publisher, &make, &artifact_name, &mut warnings)?;
141
237
  (None, Some(github))
142
238
  }
143
239
  };
144
240
 
145
241
  Ok(PublishReport {
146
242
  make,
147
- publisher: args.publisher.as_str().to_string(),
148
- channel: args.channel.clone(),
243
+ publisher: publisher.target.as_str().to_string(),
244
+ publisher_kind: publisher.target,
245
+ channel: publisher.channel.clone(),
149
246
  local,
150
247
  github,
248
+ force_publish: publisher.force_publish,
249
+ github_auth_token: publisher.github_auth_token.clone(),
151
250
  skip_make: args.skip_make,
152
251
  dry_run: args.dry_run,
153
252
  status: PublishStatus::Planned,
@@ -158,13 +257,13 @@ fn build_report(args: &PublishArgs) -> Result<PublishReport> {
158
257
 
159
258
  fn build_local_plan(
160
259
  root: &Path,
161
- args: &PublishArgs,
260
+ publisher: &ResolvedPublisher,
162
261
  make: &MakeReport,
163
262
  artifact_name: &str,
164
263
  ) -> Result<LocalPublishPlan> {
165
- let publish_root = resolve_destination(root, &args.to);
264
+ let publish_root = resolve_destination(root, &publisher.to);
166
265
  let destination_dir = publish_root
167
- .join(&args.channel)
266
+ .join(&publisher.channel)
168
267
  .join(make.package().platform())
169
268
  .join(make.package().arch());
170
269
  let destination_artifact = destination_dir.join(artifact_name);
@@ -178,12 +277,12 @@ fn build_local_plan(
178
277
  }
179
278
 
180
279
  fn build_github_plan(
181
- args: &PublishArgs,
280
+ publisher: &ResolvedPublisher,
182
281
  make: &MakeReport,
183
282
  artifact_name: &str,
184
283
  warnings: &mut Vec<String>,
185
284
  ) -> Result<GithubPublishPlan> {
186
- let repo = args
285
+ let repo = publisher
187
286
  .github_repo
188
287
  .clone()
189
288
  .or_else(|| {
@@ -204,44 +303,359 @@ fn build_github_plan(
204
303
  ));
205
304
  }
206
305
 
207
- let tag = args
208
- .github_tag
209
- .clone()
210
- .unwrap_or_else(|| default_github_tag(make, &args.channel));
211
- let release_name = args
306
+ let tag = publisher.github_tag.clone().unwrap_or_else(|| {
307
+ default_github_tag(
308
+ make,
309
+ &publisher.channel,
310
+ publisher.github_tag_prefix.as_deref(),
311
+ )
312
+ });
313
+ let release_name = publisher
212
314
  .github_release_name
213
315
  .clone()
214
316
  .unwrap_or_else(|| tag.clone());
215
- let prerelease = args.github_prerelease || tag.contains('-');
317
+ let prerelease = publisher.github_prerelease || tag.contains('-');
216
318
 
217
319
  Ok(GithubPublishPlan {
218
320
  repo,
219
321
  tag,
220
322
  release_name,
221
- draft: args.github_draft,
323
+ draft: publisher.github_draft,
222
324
  prerelease,
223
- api_url: args.github_api_url.trim_end_matches('/').to_string(),
325
+ api_url: publisher.github_api_url.trim_end_matches('/').to_string(),
224
326
  artifact_name: artifact_name.to_string(),
225
327
  release_url: None,
226
328
  asset_url: None,
227
329
  })
228
330
  }
229
331
 
332
+ fn make_args(args: &PublishArgs) -> MakeArgs {
333
+ MakeArgs {
334
+ cwd: args.cwd.clone(),
335
+ out_dir: args.out_dir.clone(),
336
+ name: args.name.clone(),
337
+ platform: args.platform.clone(),
338
+ arch: args.arch.clone(),
339
+ target: args.target,
340
+ skip_package: false,
341
+ force: args.force,
342
+ dry_run: false,
343
+ json: false,
344
+ }
345
+ }
346
+
347
+ fn resolve_publishers(
348
+ project: &ProjectSnapshot,
349
+ args: &PublishArgs,
350
+ platform: &str,
351
+ ) -> Result<ResolvedPublishers> {
352
+ if let Some(target) = args.publisher {
353
+ return Ok(ResolvedPublishers {
354
+ publishers: vec![resolved_publisher_from_args(args, target, None)],
355
+ warnings: Vec::new(),
356
+ });
357
+ }
358
+
359
+ let configured = configured_publishers(project)?;
360
+ let mut warnings = Vec::new();
361
+ let mut publishers = Vec::new();
362
+
363
+ for publisher in &configured {
364
+ let Some(target) = publisher.target else {
365
+ warnings.push(format!(
366
+ "Configured publisher is not implemented yet and will be skipped: {}.",
367
+ publisher.label
368
+ ));
369
+ continue;
370
+ };
371
+ if !publisher_applies_to_platform(publisher, platform) {
372
+ continue;
373
+ }
374
+ publishers.push(resolved_publisher_from_args(args, target, Some(publisher)));
375
+ }
376
+
377
+ if publishers.is_empty() {
378
+ if !configured.is_empty() {
379
+ warnings.push(format!(
380
+ "No supported configured publishers apply to {platform}; defaulting to local. Pass --publisher to override."
381
+ ));
382
+ }
383
+ publishers.push(resolved_publisher_from_args(
384
+ args,
385
+ PublishTarget::Local,
386
+ None,
387
+ ));
388
+ }
389
+
390
+ Ok(ResolvedPublishers {
391
+ publishers,
392
+ warnings,
393
+ })
394
+ }
395
+
396
+ fn resolved_publisher_from_args(
397
+ args: &PublishArgs,
398
+ target: PublishTarget,
399
+ configured: Option<&ConfiguredPublisher>,
400
+ ) -> ResolvedPublisher {
401
+ let default_to = PathBuf::from("out/publish/local");
402
+ let default_channel = "default".to_string();
403
+ let default_github_api_url = "https://api.github.com".to_string();
404
+
405
+ ResolvedPublisher {
406
+ target,
407
+ to: args
408
+ .to
409
+ .clone()
410
+ .or_else(|| configured.and_then(|publisher| publisher.to.clone()))
411
+ .unwrap_or(default_to),
412
+ channel: args
413
+ .channel
414
+ .clone()
415
+ .or_else(|| configured.and_then(|publisher| publisher.channel.clone()))
416
+ .unwrap_or(default_channel),
417
+ github_repo: args
418
+ .github_repo
419
+ .clone()
420
+ .or_else(|| configured.and_then(|publisher| publisher.github_repo.clone())),
421
+ github_tag: args
422
+ .github_tag
423
+ .clone()
424
+ .or_else(|| configured.and_then(|publisher| publisher.github_tag.clone())),
425
+ github_tag_prefix: configured.and_then(|publisher| publisher.github_tag_prefix.clone()),
426
+ github_release_name: args
427
+ .github_release_name
428
+ .clone()
429
+ .or_else(|| configured.and_then(|publisher| publisher.github_release_name.clone())),
430
+ github_draft: args.github_draft
431
+ || configured
432
+ .and_then(|publisher| publisher.github_draft)
433
+ .unwrap_or(false),
434
+ github_prerelease: args.github_prerelease
435
+ || configured
436
+ .and_then(|publisher| publisher.github_prerelease)
437
+ .unwrap_or(false),
438
+ github_api_url: args
439
+ .github_api_url
440
+ .clone()
441
+ .or_else(|| configured.and_then(|publisher| publisher.github_api_url.clone()))
442
+ .unwrap_or(default_github_api_url),
443
+ github_auth_token: configured.and_then(|publisher| publisher.github_auth_token.clone()),
444
+ force_publish: args.force
445
+ || configured
446
+ .and_then(|publisher| publisher.force_publish)
447
+ .unwrap_or(false),
448
+ }
449
+ }
450
+
451
+ fn configured_publishers(project: &ProjectSnapshot) -> Result<Vec<ConfiguredPublisher>> {
452
+ let Some(package_json_path) = &project.package_json else {
453
+ return Ok(Vec::new());
454
+ };
455
+ let package_json_path = Path::new(package_json_path.as_str());
456
+ let raw = fs::read_to_string(package_json_path)
457
+ .with_context(|| format!("Could not read {}", package_json_path.display()))?;
458
+ let package = serde_json::from_str::<JsonValue>(&raw)
459
+ .with_context(|| format!("Could not parse {}", package_json_path.display()))?;
460
+
461
+ let mut publishers = Vec::new();
462
+ for value in [
463
+ package
464
+ .get("config")
465
+ .and_then(|config| config.get("forge"))
466
+ .and_then(|forge| forge.get("publishers")),
467
+ package
468
+ .get("electronCli")
469
+ .or_else(|| package.get("electron-cli"))
470
+ .and_then(|config| config.get("publishers")),
471
+ ]
472
+ .into_iter()
473
+ .flatten()
474
+ {
475
+ publishers.extend(parse_publisher_list(value));
476
+ }
477
+
478
+ Ok(publishers)
479
+ }
480
+
481
+ fn parse_publisher_list(value: &JsonValue) -> Vec<ConfiguredPublisher> {
482
+ match value {
483
+ JsonValue::Array(values) => values.iter().filter_map(parse_publisher).collect(),
484
+ _ => Vec::new(),
485
+ }
486
+ }
487
+
488
+ fn parse_publisher(value: &JsonValue) -> Option<ConfiguredPublisher> {
489
+ match value {
490
+ JsonValue::String(label) => Some(ConfiguredPublisher {
491
+ label: label.clone(),
492
+ target: publisher_target(label),
493
+ platforms: Vec::new(),
494
+ to: None,
495
+ channel: None,
496
+ github_repo: None,
497
+ github_tag: None,
498
+ github_tag_prefix: None,
499
+ github_release_name: None,
500
+ github_draft: None,
501
+ github_prerelease: None,
502
+ github_api_url: None,
503
+ github_auth_token: None,
504
+ force_publish: None,
505
+ }),
506
+ JsonValue::Object(object) => {
507
+ let label = object
508
+ .get("name")
509
+ .or_else(|| object.get("publisher"))
510
+ .or_else(|| object.get("target"))
511
+ .and_then(JsonValue::as_str)?
512
+ .to_string();
513
+ Some(ConfiguredPublisher {
514
+ target: publisher_target(&label),
515
+ platforms: string_values(object.get("platforms")),
516
+ to: publisher_config_string(object, &["to", "path", "dir", "directory"])
517
+ .map(PathBuf::from),
518
+ channel: publisher_config_string(object, &["channel"]),
519
+ github_repo: publisher_config_github_repo(object),
520
+ github_tag: publisher_config_string(object, &["tag", "tagName", "tag_name"]),
521
+ github_tag_prefix: publisher_config_string(object, &["tagPrefix", "tag_prefix"]),
522
+ github_release_name: publisher_config_string(
523
+ object,
524
+ &["releaseName", "release_name"],
525
+ ),
526
+ github_draft: publisher_config_bool(object, &["draft"]),
527
+ github_prerelease: publisher_config_bool(object, &["prerelease", "preRelease"]),
528
+ github_api_url: publisher_config_api_url(object),
529
+ github_auth_token: publisher_config_string(object, &["authToken", "auth_token"]),
530
+ force_publish: publisher_config_bool(object, &["force"]),
531
+ label,
532
+ })
533
+ }
534
+ _ => None,
535
+ }
536
+ }
537
+
538
+ fn publisher_target(label: &str) -> Option<PublishTarget> {
539
+ let label = label.trim().to_ascii_lowercase();
540
+ let compact = label
541
+ .trim_start_matches("@electron-forge/")
542
+ .trim_start_matches("electron-forge-")
543
+ .trim_start_matches("publisher-");
544
+
545
+ if compact == "github"
546
+ || label.ends_with("/publisher-github")
547
+ || label.ends_with("publisher-github")
548
+ {
549
+ Some(PublishTarget::Github)
550
+ } else if compact == "local"
551
+ || label.ends_with("/publisher-local")
552
+ || label.ends_with("publisher-local")
553
+ {
554
+ Some(PublishTarget::Local)
555
+ } else {
556
+ None
557
+ }
558
+ }
559
+
560
+ fn publisher_applies_to_platform(publisher: &ConfiguredPublisher, platform: &str) -> bool {
561
+ publisher.platforms.is_empty()
562
+ || publisher
563
+ .platforms
564
+ .iter()
565
+ .any(|configured| configured == platform || configured == "*")
566
+ }
567
+
568
+ fn string_values(value: Option<&JsonValue>) -> Vec<String> {
569
+ match value {
570
+ Some(JsonValue::String(value)) => vec![value.clone()],
571
+ Some(JsonValue::Array(values)) => values
572
+ .iter()
573
+ .filter_map(JsonValue::as_str)
574
+ .map(ToOwned::to_owned)
575
+ .collect(),
576
+ _ => Vec::new(),
577
+ }
578
+ }
579
+
580
+ fn publisher_config_string(object: &JsonMap<String, JsonValue>, keys: &[&str]) -> Option<String> {
581
+ keys.iter().find_map(|key| {
582
+ publisher_config_value(object, key)
583
+ .and_then(JsonValue::as_str)
584
+ .map(str::trim)
585
+ .filter(|value| !value.is_empty())
586
+ .map(ToOwned::to_owned)
587
+ })
588
+ }
589
+
590
+ fn publisher_config_bool(object: &JsonMap<String, JsonValue>, keys: &[&str]) -> Option<bool> {
591
+ keys.iter()
592
+ .find_map(|key| publisher_config_value(object, key).and_then(JsonValue::as_bool))
593
+ }
594
+
595
+ fn publisher_config_value<'a>(
596
+ object: &'a JsonMap<String, JsonValue>,
597
+ key: &str,
598
+ ) -> Option<&'a JsonValue> {
599
+ object
600
+ .get("config")
601
+ .and_then(JsonValue::as_object)
602
+ .and_then(|config| config.get(key))
603
+ .or_else(|| object.get(key))
604
+ }
605
+
606
+ fn publisher_config_github_repo(object: &JsonMap<String, JsonValue>) -> Option<String> {
607
+ ["repository", "repo", "githubRepo", "github_repo"]
608
+ .iter()
609
+ .find_map(|key| publisher_config_value(object, key).and_then(github_repo_from_config_value))
610
+ }
611
+
612
+ fn github_repo_from_config_value(value: &JsonValue) -> Option<String> {
613
+ match value {
614
+ JsonValue::String(value) => {
615
+ let value = value.trim();
616
+ (!value.is_empty()).then(|| value.to_string())
617
+ }
618
+ JsonValue::Object(object) => {
619
+ let owner = object
620
+ .get("owner")
621
+ .or_else(|| object.get("user"))
622
+ .and_then(JsonValue::as_str)?
623
+ .trim();
624
+ let name = object
625
+ .get("name")
626
+ .or_else(|| object.get("repo"))
627
+ .and_then(JsonValue::as_str)?
628
+ .trim();
629
+ if owner.is_empty() || name.is_empty() {
630
+ None
631
+ } else {
632
+ Some(format!("{owner}/{name}"))
633
+ }
634
+ }
635
+ _ => None,
636
+ }
637
+ }
638
+
639
+ fn publisher_config_api_url(object: &JsonMap<String, JsonValue>) -> Option<String> {
640
+ publisher_config_string(object, &["apiUrl", "api_url", "baseUrl", "base_url"]).or_else(|| {
641
+ publisher_config_value(object, "octokitOptions")
642
+ .and_then(JsonValue::as_object)
643
+ .and_then(|octokit| {
644
+ octokit
645
+ .get("baseUrl")
646
+ .or_else(|| octokit.get("base_url"))
647
+ .and_then(JsonValue::as_str)
648
+ })
649
+ .map(str::trim)
650
+ .filter(|value| !value.is_empty())
651
+ .map(ToOwned::to_owned)
652
+ })
653
+ }
654
+
655
+ #[cfg(test)]
230
656
  fn execute_publish(report: &mut PublishReport, args: &PublishArgs) -> Result<()> {
231
657
  if !args.skip_make {
232
- let make_args = MakeArgs {
233
- cwd: args.cwd.clone(),
234
- out_dir: args.out_dir.clone(),
235
- name: args.name.clone(),
236
- platform: args.platform.clone(),
237
- arch: args.arch.clone(),
238
- target: args.target,
239
- skip_package: false,
240
- force: args.force,
241
- dry_run: false,
242
- json: false,
243
- };
244
- make::execute_make(&mut report.make, &make_args)?;
658
+ make::execute_make(&mut report.make, &make_args(args))?;
245
659
  report.make.mark_made()?;
246
660
  } else if !Path::new(report.make.artifact().as_str()).exists() {
247
661
  bail!(
@@ -252,18 +666,68 @@ fn execute_publish(report: &mut PublishReport, args: &PublishArgs) -> Result<()>
252
666
 
253
667
  let published_at_unix_seconds = now_unix_seconds()?;
254
668
  report.published_at_unix_seconds = Some(published_at_unix_seconds);
669
+ execute_publish_destination(report, published_at_unix_seconds)?;
670
+ report.status = PublishStatus::Published;
255
671
 
256
- match args.publisher {
257
- PublishTarget::Local => execute_local_publish(report, args, published_at_unix_seconds),
258
- PublishTarget::Github => execute_github_publish(report, args),
259
- }
672
+ Ok(())
260
673
  }
261
674
 
262
- fn execute_local_publish(
263
- report: &PublishReport,
675
+ fn execute_publish_reports(
676
+ reports: &mut [PublishReport],
677
+ make_reports: &mut [MakeReport],
264
678
  args: &PublishArgs,
679
+ ) -> Result<()> {
680
+ if !args.skip_make {
681
+ make::execute_make_reports(make_reports, &make_args(args))?;
682
+ sync_make_reports(reports, make_reports);
683
+ } else {
684
+ ensure_make_artifacts_exist(make_reports)?;
685
+ }
686
+
687
+ let published_at_unix_seconds = now_unix_seconds()?;
688
+ for report in reports {
689
+ report.published_at_unix_seconds = Some(published_at_unix_seconds);
690
+ execute_publish_destination(report, published_at_unix_seconds)?;
691
+ report.status = PublishStatus::Published;
692
+ }
693
+
694
+ Ok(())
695
+ }
696
+
697
+ fn sync_make_reports(reports: &mut [PublishReport], make_reports: &[MakeReport]) {
698
+ for report in reports {
699
+ if let Some(make) = make_reports
700
+ .iter()
701
+ .find(|make| make.artifact().as_str() == report.make.artifact().as_str())
702
+ {
703
+ report.make = make.clone();
704
+ }
705
+ }
706
+ }
707
+
708
+ fn ensure_make_artifacts_exist(make_reports: &[MakeReport]) -> Result<()> {
709
+ for make in make_reports {
710
+ if !Path::new(make.artifact().as_str()).exists() {
711
+ bail!(
712
+ "Make artifact does not exist: {}. Run without --skip-make or run electron-cli make first.",
713
+ make.artifact()
714
+ );
715
+ }
716
+ }
717
+ Ok(())
718
+ }
719
+
720
+ fn execute_publish_destination(
721
+ report: &mut PublishReport,
265
722
  published_at_unix_seconds: u64,
266
723
  ) -> Result<()> {
724
+ match report.publisher_kind {
725
+ PublishTarget::Local => execute_local_publish(report, published_at_unix_seconds),
726
+ PublishTarget::Github => execute_github_publish(report),
727
+ }
728
+ }
729
+
730
+ fn execute_local_publish(report: &PublishReport, published_at_unix_seconds: u64) -> Result<()> {
267
731
  let local = report
268
732
  .local
269
733
  .as_ref()
@@ -273,7 +737,7 @@ fn execute_local_publish(
273
737
 
274
738
  for path in [destination_artifact, manifest] {
275
739
  if path.exists() {
276
- if args.force {
740
+ if report.force_publish {
277
741
  fs::remove_file(path)
278
742
  .with_context(|| format!("Could not remove {}", path.display()))?;
279
743
  } else {
@@ -303,10 +767,10 @@ fn execute_local_publish(
303
767
  Ok(())
304
768
  }
305
769
 
306
- fn execute_github_publish(report: &mut PublishReport, args: &PublishArgs) -> Result<()> {
307
- let token = github_token()?;
770
+ fn execute_github_publish(report: &mut PublishReport) -> Result<()> {
771
+ let token = github_token(report.github_auth_token.as_deref())?;
308
772
  let agent = github_agent();
309
- publish_to_github(report, args, &token, &agent)
773
+ publish_to_github(report, &token, &agent)
310
774
  }
311
775
 
312
776
  #[derive(Debug, Serialize)]
@@ -338,12 +802,7 @@ struct GithubErrorBody {
338
802
  message: Option<String>,
339
803
  }
340
804
 
341
- fn publish_to_github(
342
- report: &mut PublishReport,
343
- args: &PublishArgs,
344
- token: &str,
345
- agent: &ureq::Agent,
346
- ) -> Result<()> {
805
+ fn publish_to_github(report: &mut PublishReport, token: &str, agent: &ureq::Agent) -> Result<()> {
347
806
  let artifact_path = Path::new(report.make.artifact().as_str());
348
807
  let github = report
349
808
  .github
@@ -359,7 +818,7 @@ fn publish_to_github(
359
818
  .iter()
360
819
  .find(|asset| asset.name == github.artifact_name)
361
820
  {
362
- if args.force {
821
+ if report.force_publish {
363
822
  delete_github_asset(agent, token, github, asset.id)?;
364
823
  } else {
365
824
  bail!(
@@ -511,9 +970,13 @@ fn github_agent() -> ureq::Agent {
511
970
  .into()
512
971
  }
513
972
 
514
- fn github_token() -> Result<String> {
515
- std::env::var("GITHUB_TOKEN")
516
- .or_else(|_| std::env::var("GH_TOKEN"))
973
+ fn github_token(configured_token: Option<&str>) -> Result<String> {
974
+ configured_token
975
+ .map(str::trim)
976
+ .filter(|token| !token.is_empty())
977
+ .map(ToOwned::to_owned)
978
+ .or_else(|| std::env::var("GITHUB_TOKEN").ok())
979
+ .or_else(|| std::env::var("GH_TOKEN").ok())
517
980
  .context("GitHub publisher requires GITHUB_TOKEN or GH_TOKEN")
518
981
  }
519
982
 
@@ -608,6 +1071,85 @@ fn print_report(report: &PublishReport, json: bool) -> Result<()> {
608
1071
  Ok(())
609
1072
  }
610
1073
 
1074
+ fn print_reports(reports: &[PublishReport], json: bool, status: PublishStatus) -> Result<()> {
1075
+ if reports.len() == 1 {
1076
+ return print_report(&reports[0], json);
1077
+ }
1078
+
1079
+ let warnings = combined_warnings(reports);
1080
+ if json {
1081
+ return output::json(&PublishRunReport {
1082
+ publishes: reports,
1083
+ dry_run: reports.iter().any(|report| report.dry_run),
1084
+ status,
1085
+ warnings,
1086
+ });
1087
+ }
1088
+
1089
+ println!("electron-cli publish");
1090
+ println!();
1091
+ if let Some(first) = reports.first() {
1092
+ println!("Project");
1093
+ println!(" root: {}", first.make.package().project().root);
1094
+ match first.make.package().project().package_label() {
1095
+ Some(label) => println!(" package: {label}"),
1096
+ None => println!(" package: not found"),
1097
+ }
1098
+ println!(" app name: {}", first.make.package().app_name());
1099
+ println!(
1100
+ " target platform: {} {}",
1101
+ first.make.package().platform(),
1102
+ first.make.package().arch()
1103
+ );
1104
+ println!(" status: {}", status.as_str());
1105
+ }
1106
+
1107
+ println!();
1108
+ println!("Publishes");
1109
+ for report in reports {
1110
+ println!(
1111
+ " {} {}: {}",
1112
+ report.publisher,
1113
+ report.make.target(),
1114
+ report.make.artifact()
1115
+ );
1116
+ if let Some(local) = &report.local {
1117
+ println!(" artifact: {}", local.destination_artifact);
1118
+ println!(" manifest: {}", local.manifest);
1119
+ }
1120
+ if let Some(github) = &report.github {
1121
+ println!(" repository: {}", github.repo);
1122
+ println!(" tag: {}", github.tag);
1123
+ if let Some(url) = &github.release_url {
1124
+ println!(" release url: {url}");
1125
+ }
1126
+ if let Some(url) = &github.asset_url {
1127
+ println!(" asset url: {url}");
1128
+ }
1129
+ }
1130
+ }
1131
+
1132
+ if !warnings.is_empty() {
1133
+ println!();
1134
+ println!("Warnings");
1135
+ for warning in warnings {
1136
+ println!(" {warning}");
1137
+ }
1138
+ }
1139
+
1140
+ Ok(())
1141
+ }
1142
+
1143
+ fn combined_warnings(reports: &[PublishReport]) -> Vec<String> {
1144
+ let mut warnings = Vec::new();
1145
+ for warning in reports.iter().flat_map(|report| report.warnings.iter()) {
1146
+ if !warnings.contains(warning) {
1147
+ warnings.push(warning.clone());
1148
+ }
1149
+ }
1150
+ warnings
1151
+ }
1152
+
611
1153
  fn resolve_destination(root: &Path, destination: &Path) -> PathBuf {
612
1154
  if destination.is_absolute() {
613
1155
  destination.to_path_buf()
@@ -616,16 +1158,17 @@ fn resolve_destination(root: &Path, destination: &Path) -> PathBuf {
616
1158
  }
617
1159
  }
618
1160
 
619
- fn default_github_tag(make: &MakeReport, channel: &str) -> String {
1161
+ fn default_github_tag(make: &MakeReport, channel: &str, tag_prefix: Option<&str>) -> String {
620
1162
  make.package()
621
1163
  .project()
622
1164
  .version
623
1165
  .as_deref()
624
1166
  .map(|version| {
625
- if version.starts_with('v') {
1167
+ let prefix = tag_prefix.unwrap_or("v");
1168
+ if prefix.is_empty() || version.starts_with(prefix) {
626
1169
  version.to_string()
627
1170
  } else {
628
- format!("v{version}")
1171
+ format!("{prefix}{version}")
629
1172
  }
630
1173
  })
631
1174
  .unwrap_or_else(|| channel.to_string())
@@ -807,6 +1350,106 @@ mod tests {
807
1350
  let _ = fs::remove_dir_all(root);
808
1351
  }
809
1352
 
1353
+ #[test]
1354
+ fn builds_github_publish_report_from_configured_forge_publisher() {
1355
+ let root = unique_temp_dir("configured-github-plan");
1356
+ write_package_json_with_publishers(
1357
+ &root,
1358
+ r#"[
1359
+ {
1360
+ "name":"@electron-forge/publisher-github",
1361
+ "platforms":["*"],
1362
+ "config":{
1363
+ "repository":{"owner":"Ikana","name":"electron-cli"},
1364
+ "draft":true,
1365
+ "prerelease":true,
1366
+ "tagPrefix":"release-",
1367
+ "releaseName":"Configured Release",
1368
+ "baseUrl":"http://127.0.0.1:9"
1369
+ }
1370
+ }
1371
+ ]"#,
1372
+ );
1373
+ write_app_file(&root);
1374
+ write_fake_electron_dist(&root);
1375
+
1376
+ let mut args = publish_args(root.clone(), true);
1377
+ args.target = None;
1378
+ args.publisher = None;
1379
+ args.github_api_url = None;
1380
+ args.channel = None;
1381
+ let report = build_report(&args).expect("report should build");
1382
+
1383
+ assert_eq!(report.publisher, "github");
1384
+ assert!(report.local.is_none());
1385
+ assert_eq!(report.channel, "default");
1386
+ let github = report.github.as_ref().expect("github plan should exist");
1387
+ assert_eq!(github.repo, "Ikana/electron-cli");
1388
+ assert_eq!(github.tag, "release-0.1.0");
1389
+ assert_eq!(github.release_name, "Configured Release");
1390
+ assert!(github.draft);
1391
+ assert!(github.prerelease);
1392
+ assert_eq!(github.api_url, "http://127.0.0.1:9");
1393
+
1394
+ let _ = fs::remove_dir_all(root);
1395
+ }
1396
+
1397
+ #[test]
1398
+ fn builds_publish_reports_from_configured_makers_and_publishers() {
1399
+ let root = unique_temp_dir("configured-publishers");
1400
+ write_package_json_with_makers_and_publishers(
1401
+ &root,
1402
+ r#"[
1403
+ {"name":"@electron-forge/maker-zip"},
1404
+ {"name":"@electron-forge/maker-deb","platforms":["linux"]}
1405
+ ]"#,
1406
+ r#"[
1407
+ {"name":"@electron-forge/publisher-github","config":{"repository":"Ikana/electron-cli"}},
1408
+ {"name":"local","config":{"to":"dist/publish","channel":"beta"}},
1409
+ {"name":"@electron-forge/publisher-s3"}
1410
+ ]"#,
1411
+ );
1412
+ write_app_file(&root);
1413
+ write_fake_electron_dist(&root);
1414
+
1415
+ let mut args = publish_args(root.clone(), true);
1416
+ args.platform = Some("linux".to_string());
1417
+ args.arch = Some("x64".to_string());
1418
+ args.target = None;
1419
+ args.publisher = None;
1420
+ args.to = None;
1421
+ args.channel = None;
1422
+ args.github_api_url = None;
1423
+ let reports = build_reports(&args).expect("reports should build");
1424
+
1425
+ assert_eq!(reports.len(), 4);
1426
+ assert_eq!(reports[0].publisher, "github");
1427
+ assert_eq!(reports[0].make.target(), "zip");
1428
+ assert_eq!(reports[1].publisher, "github");
1429
+ assert_eq!(reports[1].make.target(), "deb");
1430
+ assert_eq!(reports[2].publisher, "local");
1431
+ assert_eq!(reports[2].make.target(), "zip");
1432
+ assert_eq!(reports[3].publisher, "local");
1433
+ assert_eq!(reports[3].make.target(), "deb");
1434
+ let local = reports[2].local.as_ref().expect("local plan should exist");
1435
+ let local_parent = Path::new(local.destination_artifact.as_str())
1436
+ .parent()
1437
+ .expect("local artifact should have parent");
1438
+ assert!(local_parent.ends_with(
1439
+ PathBuf::from("dist")
1440
+ .join("publish")
1441
+ .join("beta")
1442
+ .join("linux")
1443
+ .join("x64")
1444
+ ));
1445
+ assert!(reports[0]
1446
+ .warnings
1447
+ .iter()
1448
+ .any(|warning| warning.contains("@electron-forge/publisher-s3")));
1449
+
1450
+ let _ = fs::remove_dir_all(root);
1451
+ }
1452
+
810
1453
  #[test]
811
1454
  fn publishes_make_artifact_to_github_release() {
812
1455
  let server = MockGithubServer::new(3);
@@ -824,7 +1467,7 @@ mod tests {
824
1467
  fs::write(artifact, b"artifact bytes").expect("artifact should be written");
825
1468
 
826
1469
  let agent = github_agent();
827
- publish_to_github(&mut report, &args, "test-token", &agent)
1470
+ publish_to_github(&mut report, "test-token", &agent)
828
1471
  .expect("github publish should succeed");
829
1472
 
830
1473
  let github = report.github.as_ref().expect("github plan should exist");
@@ -903,16 +1546,16 @@ mod tests {
903
1546
  name: None,
904
1547
  platform: None,
905
1548
  arch: None,
906
- target: crate::cli::MakeTarget::Zip,
907
- publisher: crate::cli::PublishTarget::Local,
908
- to: PathBuf::from("out/publish/local"),
1549
+ target: Some(crate::cli::MakeTarget::Zip),
1550
+ publisher: Some(crate::cli::PublishTarget::Local),
1551
+ to: Some(PathBuf::from("out/publish/local")),
909
1552
  github_repo: None,
910
1553
  github_tag: None,
911
1554
  github_release_name: None,
912
1555
  github_draft: false,
913
1556
  github_prerelease: false,
914
- github_api_url: "https://api.github.com".to_string(),
915
- channel: "default".to_string(),
1557
+ github_api_url: Some("https://api.github.com".to_string()),
1558
+ channel: Some("default".to_string()),
916
1559
  skip_make: false,
917
1560
  force: false,
918
1561
  dry_run,
@@ -922,8 +1565,8 @@ mod tests {
922
1565
 
923
1566
  fn github_publish_args(root: PathBuf, dry_run: bool, api_url: &str) -> PublishArgs {
924
1567
  let mut args = publish_args(root, dry_run);
925
- args.publisher = crate::cli::PublishTarget::Github;
926
- args.github_api_url = api_url.to_string();
1568
+ args.publisher = Some(crate::cli::PublishTarget::Github);
1569
+ args.github_api_url = Some(api_url.to_string());
927
1570
  args
928
1571
  }
929
1572
 
@@ -943,6 +1586,38 @@ mod tests {
943
1586
  .expect("package.json should be written");
944
1587
  }
945
1588
 
1589
+ fn write_package_json_with_publishers(root: &Path, publishers: &str) {
1590
+ fs::write(
1591
+ root.join("package.json"),
1592
+ format!(
1593
+ r#"{{
1594
+ "name":"starter-app",
1595
+ "version":"0.1.0",
1596
+ "main":"src/main.js",
1597
+ "devDependencies":{{"electron":"30.0.0"}},
1598
+ "config":{{"forge":{{"publishers":{publishers}}}}}
1599
+ }}"#
1600
+ ),
1601
+ )
1602
+ .expect("package.json with publishers should be written");
1603
+ }
1604
+
1605
+ fn write_package_json_with_makers_and_publishers(root: &Path, makers: &str, publishers: &str) {
1606
+ fs::write(
1607
+ root.join("package.json"),
1608
+ format!(
1609
+ r#"{{
1610
+ "name":"starter-app",
1611
+ "version":"0.1.0",
1612
+ "main":"src/main.js",
1613
+ "devDependencies":{{"electron":"30.0.0"}},
1614
+ "config":{{"forge":{{"makers":{makers},"publishers":{publishers}}}}}
1615
+ }}"#
1616
+ ),
1617
+ )
1618
+ .expect("package.json with makers and publishers should be written");
1619
+ }
1620
+
946
1621
  fn write_app_file(root: &Path) {
947
1622
  fs::create_dir_all(root.join("src")).expect("src should be created");
948
1623
  fs::write(root.join("src/main.js"), "console.log('hello');")