Repository: electron-userland/electron-forge (ts)

Issue:
Add Snap (snapcraft) Maker
See: http://snapcraft.io/

I would rather not have code that calls snap inside of Electron Forge, but rather it should be a separate package like the ones for `.deb`, `.rpm`, and Flatpak. (`electron-installer-snap`?)

Pull request description:
Integrated Snap support
* [x] I have read the [contribution documentation](https://github.com/electron-userland/electron-forge/blob/master/CONTRIBUTING.md) for this project.
* [x] I agree to follow the [code of conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md) that this project follows, as appropriate.
* [x] The changes are appropriately documented (if applicable).
* [x] The changes have sufficient test coverage (if applicable).
* [x] The testsuite passes successfully on my local machine (if applicable).

**Summarize your changes:**

Adds builtin support for making Snap distributables, and publishing them to the Snap store. Both are accomplished via [`electron-installer-snap`](https://github.com/electron-userland/electron-installer-snap) and the result of work done during the September 2017 Ubuntu Rally and January 2018 Snapcraft Summit.

Closes #25.

Patch:
diff --git a/README.md b/README.md
index 5d85d5d3f..6e150feda 100644
--- a/README.md
+++ b/README.md
@@ -162,7 +162,7 @@ config object:
   "make_targets": {
     "win32": ["squirrel"], // An array of win32 make targets
     "darwin": ["zip", "dmg"], // An array of darwin make targets
-    "linux": ["deb", "rpm", "flatpak"] // An array of linux make targets
+    "linux": ["deb", "rpm", "flatpak", "snap"] // An array of linux make targets
   },
   "electronPackagerConfig": {},
   "electronRebuildConfig": {},
@@ -170,7 +170,8 @@ config object:
   "electronInstallerDMG": {},
   "electronInstallerFlatpak": {},
   "electronInstallerDebian": {},
-  "electronInstallerRedhat": {}
+  "electronInstallerRedhat": {},
+  "electronInstallerSnap": {}
 }
 ```
 
@@ -186,6 +187,7 @@ config object:
 | `deb`       | Linux               | Generates a Debian package | [`electronInstallerDebian`](https://github.com/unindented/electron-installer-debian#options) | Yes | [`fakeroot` and `dpkg`](https://github.com/unindented/electron-installer-debian#requirements) |
 | `rpm`       | Linux               | Generates an RPM package | [`electronInstallerRedhat`](https://github.com/unindented/electron-installer-redhat#options) | Yes | [`rpm`](https://github.com/unindented/electron-installer-redhatn#requirements) |
 | `flatpak`   | Linux               | Generates a [Flatpak](http://flatpak.org/) file | [`electronInstallerFlatpak`](https://github.com/endlessm/electron-installer-flatpak#options) | No | [`flatpak-builder`](https://github.com/endlessm/electron-installer-flatpak#requirements) |
+| `snap`      | Linux               | Generates a [Snap](https://snapcraft.io/) file | [`electronInstallerSnap`](https://github.com/electron-userland/electron-installer-snap/blob/master/docs/api.md#options) | No | [`snapcraft`](https://snapcraft.io/docs/build-snaps/#install-snapcraft) |
 
 ## Configuring `package`
 
@@ -230,6 +232,7 @@ the JS file method mentioned above then you can use functions normally.
 | GitHub Releases - `github` | Makes a new release for the current version (if required) and uploads the make artifacts as release assets | `process.env.GITHUB_TOKEN` - A personal access token with access to your releases <br />`forge.github_repository.owner` - The owner of the GitHub repository<br />`forge.github_repository.name` - The name of the GitHub repository <br />`forge.github_repository.draft` - Create the release as a draft, defaults to `true` <br />`forge.github_repository.prerelease` - Identify the release as a prerelease, defaults to `false` |
 | Amazon S3 - `s3` | Uploads your artifacts to the given S3 bucket | `process.env.ELECTRON_FORGE_S3_SECRET_ACCESS_KEY` - Your secret access token for your AWS account _(falls back to the standard `AWS_SECRET_ACCESS_KEY` environment variable)_<br />`forge.s3.accessKeyId` - Your access key for your AWS account _(falls back to the standard `AWS_ACCESS_KEY_ID` environment variable)_<br />`forge.s3.bucket` - The name of the S3 bucket to upload to<br />`forge.s3.folder` - The folder path to upload to inside your bucket, defaults to your application version<br />`forge.s3.public` - Whether to make the S3 upload public, defaults to `false` |
 | [Electron Release Server](https://github.com/ArekSredzki/electron-release-server) - `electron-release-server` |  Makes a new release for the current version and uploads the artifacts to the correct platform/arch in the given version.  If the version already exists no upload will be performed.  The channel is determined from the current version. | `forge.electronReleaseServer.baseUrl` - The base URL of your release server, no trailing slash<br />`forge.electronReleaseServer.username` - The username for the admin panel on your server<br />`forge.electronReleaseServer.password` - The password for the admin panel on your server |
+| [Snapcraft](https://snapcraft.io/store/) - `snapStore` | Uploads generated Snaps to the Snap Store. | `forge.snapStore.release` - If specified, a comma-separated list of channels to release to. |
 
 For example:
 
@@ -260,6 +263,13 @@ For example:
     "password": "no_one_will_guess_this"
   }
 }
+
+// Snap Store
+{
+  "snapStore": {
+    "release": "candidate,beta"
+  }
+}
 ```
 
 ## Custom `make` and `publish` targets
diff --git a/SUPPORT.md b/SUPPORT.md
index fecda0fea..94af6eca8 100644
--- a/SUPPORT.md
+++ b/SUPPORT.md
@@ -17,6 +17,7 @@ feature. Known modules include:
 * `electron-installer-dmg`
 * `electron-installer-flatpak`
 * `electron-installer-redhat`
+* `electron-installer-snap:*`
 * `electron-osx-sign`
 * `electron-packager`
 * `electron-rebuild`
diff --git a/package.json b/package.json
index beb435842..09d509b53 100644
--- a/package.json
+++ b/package.json
@@ -157,6 +157,7 @@
     "electron-installer-dmg": "^0.2.0",
     "electron-installer-flatpak": "^0.8.0",
     "electron-installer-redhat": "^0.5.0",
+    "electron-installer-snap": "^2.0.0",
     "electron-windows-store": "^0.12.0",
     "electron-winstaller": "^2.5.0",
     "electron-wix-msi": "^1.3.0"
diff --git a/src/makers/linux/snap.js b/src/makers/linux/snap.js
new file mode 100644
index 000000000..b214ea070
--- /dev/null
+++ b/src/makers/linux/snap.js
@@ -0,0 +1,22 @@
+import path from 'path';
+
+import { ensureDirectory } from '../../util/ensure-output';
+import configFn from '../../util/config-fn';
+
+export const isSupportedOnCurrentPlatform = async () => process.platform === 'linux';
+
+export default async ({ dir, targetArch, forgeConfig }) => {
+  const installer = require('electron-installer-snap');
+
+  const outPath = path.resolve(dir, '../make');
+
+  await ensureDirectory(outPath);
+  const snapDefaults = {
+    arch: targetArch,
+    dest: outPath,
+    src: dir,
+  };
+  const snapConfig = Object.assign({}, configFn(forgeConfig.electronInstallerSnap, targetArch), snapDefaults);
+
+  return [await installer(snapConfig)];
+};
diff --git a/src/publishers/snapcraft.js b/src/publishers/snapcraft.js
new file mode 100644
index 000000000..49ccf4153
--- /dev/null
+++ b/src/publishers/snapcraft.js
@@ -0,0 +1,28 @@
+import fs from 'fs-extra';
+import path from 'path';
+import Snapcraft from 'electron-installer-snap/snapcraft';
+
+import asyncOra from '../util/ora-handler';
+
+/**
+ * `forgeConfig.snapStore`:
+ * * `release`: comma-separated list of channels to release to
+ */
+export default async ({ dir, artifacts, forgeConfig }) => {
+  const snapArtifacts = artifacts.filter(artifact => artifact.endsWith('.snap'));
+
+  if (snapArtifacts.length === 0) {
+    throw 'No snap files to upload. Please ensure that "snap" is listed in the "make_targets" in Forge config.';
+  }
+
+  const snapcraftCfgPath = path.join(dir, '.snapcraft', 'snapcraft.cfg');
+
+  if (!await fs.pathExists(snapcraftCfgPath)) {
+    throw `Snapcraft credentials not found at "${snapcraftCfgPath}". It can be generated with the command "snapcraft export-login" (snapcraft 2.37 and above).`;
+  }
+
+  await asyncOra('Pushing snap to the snap store', async () => {
+    const snapcraft = new Snapcraft();
+    await snapcraft.run(dir, 'push', forgeConfig.snapStore, snapArtifacts);
+  });
+};


Test patch:
diff --git a/test/fast/makers/snap_spec.js b/test/fast/makers/snap_spec.js
new file mode 100644
index 000000000..51c762742
--- /dev/null
+++ b/test/fast/makers/snap_spec.js
@@ -0,0 +1,59 @@
+import chai, { expect } from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import path from 'path';
+import proxyquire from 'proxyquire';
+import { stub } from 'sinon';
+
+chai.use(chaiAsPromised);
+
+describe('snap maker', () => {
+  let snapModule;
+  let snapMaker;
+  let eisStub;
+  let ensureDirectoryStub;
+  let forgeConfig;
+
+  const dir = '/my/test/dir/out/foo-linux-x64';
+  const appName = 'My Test App';
+  const targetArch = process.arch;
+  const packageJSON = { version: '1.2.3' };
+
+  beforeEach(() => {
+    ensureDirectoryStub = stub().returns(Promise.resolve());
+    eisStub = stub().resolves('/my/test/dir/out/make/foo_0.0.1_amd64.snap');
+    forgeConfig = { electronInstallerSnap: {} };
+
+    snapModule = proxyquire.noPreserveCache().noCallThru().load('../../../src/makers/linux/snap', {
+      './config-fn': config => config,
+      '../../util/ensure-output': { ensureDirectory: ensureDirectoryStub },
+      'electron-installer-snap': eisStub,
+    });
+    snapMaker = snapModule.default;
+  });
+
+  it('should pass through correct defaults', async () => {
+    await snapMaker({ dir, appName, targetArch, forgeConfig, packageJSON });
+    const opts = eisStub.firstCall.args[0];
+    expect(opts).to.deep.equal({
+      arch: process.arch,
+      src: dir,
+      dest: path.resolve(dir, '..', 'make'),
+    });
+  });
+
+  it('should have config cascade correctly', async () => {
+    forgeConfig.electronInstallerSnap = {
+      arch: 'overridden',
+      description: 'Snap description',
+    };
+
+    await snapMaker({ dir, appName, targetArch, forgeConfig, packageJSON });
+    const opts = eisStub.firstCall.args[0];
+    expect(opts).to.deep.equal({
+      arch: process.arch,
+      src: dir,
+      dest: path.resolve(dir, '..', 'make'),
+      description: 'Snap description',
+    });
+  });
+});
diff --git a/test/fast/makers_spec.js b/test/fast/makers_spec.js
index 63d848d06..fdb8279da 100644
--- a/test/fast/makers_spec.js
+++ b/test/fast/makers_spec.js
@@ -8,6 +8,7 @@ describe('makers', () => {
       'linux/deb': ['darwin', 'linux'],
       'linux/flatpak': ['darwin', 'linux'],
       'linux/rpm': ['darwin', 'linux'],
+      'linux/snap': ['linux'],
       'win32/appx': ['win32'],
       'win32/squirrel': ['darwin', 'linux', 'win32'],
       'win32/wix': ['win32'],