@electron-forge/publisher-electron-release-server 6.0.0-beta.7 → 6.0.0-beta.70

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.
@@ -1,12 +1,12 @@
1
- import PublisherBase, { PublisherOptions } from '@electron-forge/publisher-base';
2
- import { asyncOra } from '@electron-forge/async-ora';
3
- import { ForgePlatform, ForgeArch } from '@electron-forge/shared-types';
1
+ import path from 'path';
4
2
 
3
+ import { asyncOra } from '@electron-forge/async-ora';
4
+ import { PublisherBase, PublisherOptions } from '@electron-forge/publisher-base';
5
+ import { ForgeArch, ForgePlatform } from '@electron-forge/shared-types';
5
6
  import debug from 'debug';
6
- import fetch from 'node-fetch';
7
7
  import FormData from 'form-data';
8
8
  import fs from 'fs-extra';
9
- import path from 'path';
9
+ import fetch, { RequestInfo, RequestInit, Response } from 'node-fetch';
10
10
 
11
11
  import { PublisherERSConfig } from './Config';
12
12
 
@@ -14,10 +14,20 @@ const d = debug('electron-forge:publish:ers');
14
14
 
15
15
  interface ERSVersion {
16
16
  name: string;
17
- assets: { name: string; }[];
17
+ assets: { name: string }[];
18
+ flavor?: string;
18
19
  }
19
20
 
20
- export const ersPlatform = (platform: ForgePlatform, arch: ForgeArch) => {
21
+ const fetchAndCheckStatus = async (url: RequestInfo, init?: RequestInit): Promise<Response> => {
22
+ const result = await fetch(url, init);
23
+ if (result.ok) {
24
+ // res.status >= 200 && res.status < 300
25
+ return result;
26
+ }
27
+ throw new Error(`ERS publish failed with status code: ${result.status} (${result.url})`);
28
+ };
29
+
30
+ export const ersPlatform = (platform: ForgePlatform, arch: ForgeArch): string => {
21
31
  switch (platform) {
22
32
  case 'darwin':
23
33
  return 'osx_64';
@@ -33,39 +43,45 @@ export const ersPlatform = (platform: ForgePlatform, arch: ForgeArch) => {
33
43
  export default class PublisherERS extends PublisherBase<PublisherERSConfig> {
34
44
  name = 'electron-release-server';
35
45
 
36
- async publish({ makeResults }: PublisherOptions) {
46
+ async publish({ makeResults }: PublisherOptions): Promise<void> {
37
47
  const { config } = this;
38
48
 
39
49
  if (!(config.baseUrl && config.username && config.password)) {
40
- throw 'In order to publish to ERS you must set the "electronReleaseServer.baseUrl", "electronReleaseServer.username" and "electronReleaseServer.password" properties in your forge config. See the docs for more info'; // eslint-disable-line
50
+ throw new Error(
51
+ 'In order to publish to ERS you must set the "electronReleaseServer.baseUrl", "electronReleaseServer.username" and "electronReleaseServer.password" properties in your Forge config. See the docs for more info'
52
+ );
41
53
  }
42
54
 
43
55
  d('attempting to authenticate to ERS');
44
56
 
45
57
  const api = (apiPath: string) => `${config.baseUrl}/${apiPath}`;
46
58
 
47
- const { token } = await (await fetch(api('api/auth/login'), {
48
- method: 'POST',
49
- body: JSON.stringify({
50
- username: config.username,
51
- password: config.password,
52
- }),
53
- headers: {
54
- 'Content-Type': 'application/json',
55
- },
56
- })).json();
57
-
58
- const authFetch = (apiPath: string, options?: any) =>
59
- fetch(api(apiPath), Object.assign({}, options || {}, {
60
- headers: Object.assign({}, (options || {}).headers, { Authorization: `Bearer ${token}` }),
61
- }));
59
+ const { token } = await (
60
+ await fetchAndCheckStatus(api('api/auth/login'), {
61
+ method: 'POST',
62
+ body: JSON.stringify({
63
+ username: config.username,
64
+ password: config.password,
65
+ }),
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ },
69
+ })
70
+ ).json();
71
+
72
+ const authFetch = (apiPath: string, options?: RequestInit) =>
73
+ fetchAndCheckStatus(api(apiPath), { ...(options || {}), headers: { ...(options || {}).headers, Authorization: `Bearer ${token}` } });
62
74
 
63
75
  const versions: ERSVersion[] = await (await authFetch('api/version')).json();
76
+ const flavor = config.flavor || 'default';
64
77
 
65
78
  for (const makeResult of makeResults) {
66
- const { artifacts, packageJSON } = makeResult;
79
+ const { packageJSON } = makeResult;
80
+ const artifacts = makeResult.artifacts.filter((artifactPath) => path.basename(artifactPath).toLowerCase() !== 'releases');
67
81
 
68
- const existingVersion = versions.find(version => version.name === packageJSON.version);
82
+ const existingVersion = versions.find((version) => {
83
+ return version.name === packageJSON.version && (!version.flavor || version.flavor === flavor);
84
+ });
69
85
 
70
86
  let channel = 'stable';
71
87
  if (config.channel) {
@@ -83,6 +99,7 @@ export default class PublisherERS extends PublisherBase<PublisherERSConfig> {
83
99
  channel: {
84
100
  name: channel,
85
101
  },
102
+ flavor: config.flavor,
86
103
  name: packageJSON.version,
87
104
  notes: '',
88
105
  }),
@@ -100,12 +117,10 @@ export default class PublisherERS extends PublisherBase<PublisherERSConfig> {
100
117
  uploadSpinner.text = getText();
101
118
  };
102
119
 
103
- await Promise.all(artifacts.map(artifactPath =>
104
- new Promise(async (resolve, reject) => {
120
+ await Promise.all(
121
+ artifacts.map(async (artifactPath) => {
105
122
  if (existingVersion) {
106
- const existingAsset = existingVersion.assets.find(
107
- asset => asset.name === path.basename(artifactPath),
108
- );
123
+ const existingAsset = existingVersion.assets.find((asset) => asset.name === path.basename(artifactPath));
109
124
 
110
125
  if (existingAsset) {
111
126
  d('asset at path:', artifactPath, 'already exists on server');
@@ -114,28 +129,31 @@ export default class PublisherERS extends PublisherBase<PublisherERSConfig> {
114
129
  return;
115
130
  }
116
131
  }
117
- try {
118
- d('attempting to upload asset:', artifactPath);
119
- const artifactForm = new FormData();
120
- artifactForm.append('token', token);
121
- artifactForm.append('version', packageJSON.version);
122
- artifactForm.append('platform', ersPlatform(makeResult.platform, makeResult.arch));
123
- artifactForm.append('file', fs.createReadStream(artifactPath));
124
- await authFetch('api/asset', {
125
- method: 'POST',
126
- body: artifactForm,
127
- headers: artifactForm.getHeaders(),
128
- });
129
- d('upload successful for asset:', artifactPath);
130
- uploaded += 1;
131
- updateSpinner();
132
- resolve();
133
- } catch (err) {
134
- reject(err);
135
- }
136
- }),
137
- ));
132
+ d('attempting to upload asset:', artifactPath);
133
+ const artifactForm = new FormData();
134
+ artifactForm.append('token', token);
135
+ artifactForm.append('version', packageJSON.version);
136
+ artifactForm.append('platform', ersPlatform(makeResult.platform, makeResult.arch));
137
+
138
+ // see https://github.com/form-data/form-data/issues/426
139
+ const fileOptions = {
140
+ knownLength: fs.statSync(artifactPath).size,
141
+ };
142
+ artifactForm.append('file', fs.createReadStream(artifactPath), fileOptions);
143
+
144
+ await authFetch('api/asset', {
145
+ method: 'POST',
146
+ body: artifactForm,
147
+ headers: artifactForm.getHeaders(),
148
+ });
149
+ d('upload successful for asset:', artifactPath);
150
+ uploaded += 1;
151
+ updateSpinner();
152
+ })
153
+ );
138
154
  });
139
155
  }
140
156
  }
141
157
  }
158
+
159
+ export { PublisherERS, PublisherERSConfig };
@@ -0,0 +1,218 @@
1
+ import { ForgeMakeResult, ResolvedForgeConfig } from '@electron-forge/shared-types';
2
+ import { expect } from 'chai';
3
+ import fetchMock from 'fetch-mock';
4
+ import proxyquire from 'proxyquire';
5
+ import { stub } from 'sinon';
6
+
7
+ describe('PublisherERS', () => {
8
+ let fetch: typeof fetchMock;
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ let PublisherERS: any;
11
+
12
+ beforeEach(() => {
13
+ fetch = fetchMock.sandbox();
14
+ PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
15
+ 'node-fetch': fetch,
16
+ 'fs-extra': { createReadStream: stub().returns(''), statSync: stub().returns({ size: 100 }) },
17
+ }).default;
18
+ });
19
+
20
+ afterEach(() => {
21
+ fetch.restore();
22
+ });
23
+
24
+ describe('new version', () => {
25
+ it('can publish a new version to ERS', async () => {
26
+ const baseUrl = 'https://example.com';
27
+ const token = 'FAKE_TOKEN';
28
+ const flavor = 'lite';
29
+ const version = '3.0.0';
30
+
31
+ // mock login
32
+ fetch.postOnce('path:/api/auth/login', { body: { token }, status: 200 });
33
+ // mock fetch all existing versions
34
+ fetch.getOnce('path:/api/version', { body: [{ name: '2.0.0', assets: [], flavor: 'default' }], status: 200 });
35
+ // mock creating a new version
36
+ fetch.postOnce('path:/api/version', { status: 200 });
37
+ // mock asset upload
38
+ fetch.post('path:/api/asset', { status: 200 });
39
+
40
+ const publisher = new PublisherERS({
41
+ baseUrl,
42
+ username: 'test',
43
+ password: 'test',
44
+ flavor,
45
+ });
46
+
47
+ const makeResults: ForgeMakeResult[] = [
48
+ {
49
+ artifacts: ['/path/to/artifact'],
50
+ packageJSON: {
51
+ version,
52
+ },
53
+ platform: 'linux',
54
+ arch: 'x64',
55
+ },
56
+ ];
57
+
58
+ await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ResolvedForgeConfig });
59
+
60
+ const calls = fetch.calls();
61
+
62
+ // creates a new version with the correct flavor, name, and channel
63
+ expect(calls[2][0]).to.equal(`${baseUrl}/api/version`);
64
+ expect(calls[2][1]?.body).to.equal(`{"channel":{"name":"stable"},"flavor":"${flavor}","name":"${version}","notes":""}`);
65
+
66
+ // uploads asset successfully
67
+ expect(calls[3][0]).to.equal(`${baseUrl}/api/asset`);
68
+ });
69
+ });
70
+
71
+ describe('existing version', () => {
72
+ it('can add new assets', async () => {
73
+ const baseUrl = 'https://example.com';
74
+ const token = 'FAKE_TOKEN';
75
+ const channel = 'stable';
76
+ const flavor = 'lite';
77
+ const version = '2.0.0';
78
+
79
+ // mock login
80
+ fetch.postOnce('path:/api/auth/login', { body: { token }, status: 200 });
81
+ // mock fetch all existing versions
82
+ fetch.getOnce('path:/api/version', { body: [{ name: '2.0.0', assets: [], flavor: 'lite' }], status: 200 });
83
+ // mock asset upload
84
+ fetch.post('path:/api/asset', { status: 200 });
85
+
86
+ const publisher = new PublisherERS({
87
+ baseUrl,
88
+ username: 'test',
89
+ password: 'test',
90
+ channel,
91
+ flavor,
92
+ });
93
+
94
+ const makeResults: ForgeMakeResult[] = [
95
+ {
96
+ artifacts: ['/path/to/artifact'],
97
+ packageJSON: {
98
+ version,
99
+ },
100
+ platform: 'linux',
101
+ arch: 'x64',
102
+ },
103
+ ];
104
+
105
+ await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ResolvedForgeConfig });
106
+
107
+ const calls = fetch.calls();
108
+
109
+ // uploads asset successfully
110
+ expect(calls[2][0]).to.equal(`${baseUrl}/api/asset`);
111
+ });
112
+
113
+ it('does not replace assets for existing version', async () => {
114
+ const baseUrl = 'https://example.com';
115
+ const token = 'FAKE_TOKEN';
116
+ const channel = 'stable';
117
+ const version = '2.0.0';
118
+
119
+ // mock login
120
+ fetch.postOnce('path:/api/auth/login', { body: { token }, status: 200 });
121
+ // mock fetch all existing versions
122
+ fetch.getOnce('path:/api/version', { body: [{ name: '2.0.0', assets: [{ name: 'existing-artifact' }], flavor: 'default' }], status: 200 });
123
+
124
+ const publisher = new PublisherERS({
125
+ baseUrl,
126
+ username: 'test',
127
+ password: 'test',
128
+ channel,
129
+ });
130
+
131
+ const makeResults: ForgeMakeResult[] = [
132
+ {
133
+ artifacts: ['/path/to/existing-artifact'],
134
+ packageJSON: {
135
+ version,
136
+ },
137
+ platform: 'linux',
138
+ arch: 'x64',
139
+ },
140
+ ];
141
+
142
+ await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ResolvedForgeConfig });
143
+
144
+ const calls = fetch.calls();
145
+ expect(calls).to.have.length(2);
146
+ });
147
+
148
+ it('can upload a new flavor for an existing version', async () => {
149
+ const baseUrl = 'https://example.com';
150
+ const token = 'FAKE_TOKEN';
151
+ const version = '2.0.0';
152
+ const flavor = 'lite';
153
+
154
+ // mock login
155
+ fetch.postOnce('path:/api/auth/login', { body: { token }, status: 200 });
156
+ // mock fetch all existing versions
157
+ fetch.getOnce('path:/api/version', { body: [{ name: '2.0.0', assets: [{ name: 'existing-artifact' }], flavor: 'default' }], status: 200 });
158
+ // mock creating a new version
159
+ fetch.postOnce('path:/api/version', { status: 200 });
160
+ // mock asset upload
161
+ fetch.post('path:/api/asset', { status: 200 });
162
+
163
+ const publisher = new PublisherERS({
164
+ baseUrl,
165
+ username: 'test',
166
+ password: 'test',
167
+ flavor,
168
+ });
169
+
170
+ const makeResults: ForgeMakeResult[] = [
171
+ {
172
+ artifacts: ['/path/to/artifact'],
173
+ packageJSON: {
174
+ version,
175
+ },
176
+ platform: 'linux',
177
+ arch: 'x64',
178
+ },
179
+ ];
180
+
181
+ await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ResolvedForgeConfig });
182
+
183
+ const calls = fetch.calls();
184
+
185
+ // creates a new version with the correct flavor, name, and channel
186
+ expect(calls[2][0]).to.equal(`${baseUrl}/api/version`);
187
+ expect(calls[2][1]?.body).to.equal(`{"channel":{"name":"stable"},"flavor":"${flavor}","name":"${version}","notes":""}`);
188
+
189
+ // uploads asset successfully
190
+ expect(calls[3][0]).to.equal(`${baseUrl}/api/asset`);
191
+ });
192
+
193
+ // TODO: implement edge cases
194
+ it('can read the channel from the package.json version');
195
+ it('does not upload the RELEASES file');
196
+ });
197
+
198
+ it('fails if username and password are not provided', () => {
199
+ const publisher = new PublisherERS({});
200
+
201
+ expect(publisher.publish({ makeResults: [], dir: '', forgeConfig: {} as ResolvedForgeConfig })).to.eventually.be.rejectedWith(
202
+ 'In order to publish to ERS you must set the "electronReleaseServer.baseUrl", "electronReleaseServer.username" and "electronReleaseServer.password" properties in your Forge config. See the docs for more info'
203
+ );
204
+ });
205
+
206
+ it('fails if the server returns 4xx', async () => {
207
+ fetch.mock('begin:http://example.com', { body: {}, status: 400 });
208
+
209
+ const publisher = new PublisherERS({
210
+ baseUrl: 'http://example.com',
211
+ username: 'test',
212
+ password: 'test',
213
+ });
214
+ return expect(publisher.publish({ makeResults: [], dir: '', forgeConfig: {} as ResolvedForgeConfig })).to.eventually.be.rejectedWith(
215
+ 'ERS publish failed with status code: 400 (http://example.com/api/auth/login)'
216
+ );
217
+ });
218
+ });
package/tsconfig.json CHANGED
@@ -1,23 +1,42 @@
1
1
  {
2
- "compilerOptions": {
3
- "module": "commonjs",
4
- "target": "es6",
5
- "outDir": "dist",
6
- "lib": [
7
- "es6",
8
- "dom",
9
- "es7"
10
- ],
11
- "sourceMap": true,
12
- "rootDir": "src",
13
- "experimentalDecorators": true,
14
- "strict": true,
15
- "esModuleInterop": true,
16
- "declaration": true
17
- },
18
- "exclude": [
19
- "node_modules",
20
- "dist",
21
- "test"
22
- ]
2
+ "//": "⚠️ AUTOGENERATED ⚠️ This file was automatically generated by tools/gen-tsconfigs.ts, do not edit manually.",
3
+ "compilerOptions": {
4
+ "module": "commonjs",
5
+ "target": "es2019",
6
+ "outDir": "dist",
7
+ "lib": [
8
+ "dom",
9
+ "es2019"
10
+ ],
11
+ "sourceMap": true,
12
+ "rootDir": "src",
13
+ "experimentalDecorators": true,
14
+ "strict": true,
15
+ "esModuleInterop": true,
16
+ "declaration": true,
17
+ "composite": true,
18
+ "declarationMap": true,
19
+ "typeRoots": [
20
+ "../../../typings",
21
+ "../../../node_modules/@types"
22
+ ]
23
+ },
24
+ "exclude": [
25
+ "node_modules",
26
+ "dist",
27
+ "test",
28
+ "index.ts",
29
+ "tmpl"
30
+ ],
31
+ "references": [
32
+ {
33
+ "path": "../../utils/async-ora"
34
+ },
35
+ {
36
+ "path": "../base"
37
+ },
38
+ {
39
+ "path": "../../utils/types"
40
+ }
41
+ ]
23
42
  }