Skip to content

Commit

Permalink
feat(publisher-ers): support flavor config (#2766)
Browse files Browse the repository at this point in the history
  • Loading branch information
erickzhao authored Apr 20, 2022
1 parent 4b07c1f commit 6069ebe
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 3 deletions.
8 changes: 8 additions & 0 deletions packages/publisher/electron-release-server/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,12 @@ export interface PublisherERSConfig {
* Default: stable
*/
channel?: string;

/**
* The "flavor" of the binary that you want to release to.
* This is useful if you want to provide multiple versions
* of the same application version (e.g. full and lite)
* to end users.
*/
flavor?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const d = debug('electron-forge:publish:ers');
interface ERSVersion {
name: string;
assets: { name: string }[];
flavor?: string;
}

const fetchAndCheckStatus = async (url: RequestInfo, init?: RequestInit): Promise<Response> => {
Expand Down Expand Up @@ -73,12 +74,15 @@ export default class PublisherERS extends PublisherBase<PublisherERSConfig> {
fetchAndCheckStatus(api(apiPath), { ...(options || {}), headers: { ...(options || {}).headers, Authorization: `Bearer ${token}` } });

const versions: ERSVersion[] = await (await authFetch('api/version')).json();
const flavor = config.flavor || 'default';

for (const makeResult of makeResults) {
const { packageJSON } = makeResult;
const artifacts = makeResult.artifacts.filter((artifactPath) => path.basename(artifactPath).toLowerCase() !== 'releases');

const existingVersion = versions.find((version) => version.name === packageJSON.version);
const existingVersion = versions.find((version) => {
return version.name === packageJSON.version && (!version.flavor || version.flavor === flavor);
});

let channel = 'stable';
if (config.channel) {
Expand All @@ -97,6 +101,7 @@ export default class PublisherERS extends PublisherBase<PublisherERSConfig> {
channel: {
name: channel,
},
flavor: config.flavor,
name: packageJSON.version,
notes: '',
}),
Expand Down
207 changes: 205 additions & 2 deletions packages/publisher/electron-release-server/test/PublisherERS_spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,217 @@
import { expect } from 'chai';
import { ForgeConfig } from '@electron-forge/shared-types';
import { ForgeConfig, ForgeMakeResult } from '@electron-forge/shared-types';
import fetchMock from 'fetch-mock';
import proxyquire from 'proxyquire';

describe('PublisherERS', () => {
let fetch: typeof fetchMock;

beforeEach(() => {
fetch = fetchMock.sandbox();
});
it('fail if the server returns 4xx', async () => {

describe('new version', () => {
it('can publish a new version to ERS', async () => {
const baseUrl = 'https://example.com';
const token = 'FAKE_TOKEN';
const flavor = 'lite';
const version = '3.0.0';

// mock login
fetch.postOnce('path:/api/auth/login', { body: { token }, status: 200 });
// mock fetch all existing versions
fetch.getOnce('path:/api/version', { body: [{ name: '2.0.0', assets: [], flavor: 'default' }], status: 200 });
// mock creating a new version
fetch.postOnce('path:/api/version', { status: 200 });
// mock asset upload
fetch.post('path:/api/asset', { status: 200 });
const PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
'node-fetch': fetch,
}).default;

const publisher = new PublisherERS({
baseUrl,
username: 'test',
password: 'test',
flavor,
});

const makeResults: ForgeMakeResult[] = [
{
artifacts: ['/path/to/artifact'],
packageJSON: {
version,
},
platform: 'linux',
arch: 'x64',
},
];

await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ForgeConfig });

const calls = fetch.calls();

// creates a new version with the correct flavor, name, and channel
expect(calls[2][0]).to.equal(`${baseUrl}/api/version`);
expect(calls[2][1]?.body).to.equal(`{"channel":{"name":"stable"},"flavor":"${flavor}","name":"${version}","notes":""}`);

// uploads asset successfully
expect(calls[3][0]).to.equal(`${baseUrl}/api/asset`);
});
});

describe('existing version', () => {
it('can add new assets', async () => {
const baseUrl = 'https://example.com';
const token = 'FAKE_TOKEN';
const channel = 'stable';
const flavor = 'lite';
const version = '2.0.0';

// mock login
fetch.postOnce('path:/api/auth/login', { body: { token }, status: 200 });
// mock fetch all existing versions
fetch.getOnce('path:/api/version', { body: [{ name: '2.0.0', assets: [], flavor: 'lite' }], status: 200 });
// mock asset upload
fetch.post('path:/api/asset', { status: 200 });

const PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
'node-fetch': fetch,
}).default;

const publisher = new PublisherERS({
baseUrl,
username: 'test',
password: 'test',
channel,
flavor,
});

const makeResults: ForgeMakeResult[] = [
{
artifacts: ['/path/to/artifact'],
packageJSON: {
version,
},
platform: 'linux',
arch: 'x64',
},
];

await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ForgeConfig });

const calls = fetch.calls();

// uploads asset successfully
expect(calls[2][0]).to.equal(`${baseUrl}/api/asset`);
});

it('does not replace assets for existing version', async () => {
const baseUrl = 'https://example.com';
const token = 'FAKE_TOKEN';
const channel = 'stable';
const version = '2.0.0';

// mock login
fetch.postOnce('path:/api/auth/login', { body: { token }, status: 200 });
// mock fetch all existing versions
fetch.getOnce('path:/api/version', { body: [{ name: '2.0.0', assets: [{ name: 'existing-artifact' }], flavor: 'default' }], status: 200 });

const PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
'node-fetch': fetch,
}).default;

const publisher = new PublisherERS({
baseUrl,
username: 'test',
password: 'test',
channel,
});

const makeResults: ForgeMakeResult[] = [
{
artifacts: ['/path/to/existing-artifact'],
packageJSON: {
version,
},
platform: 'linux',
arch: 'x64',
},
];

await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ForgeConfig });

const calls = fetch.calls();
expect(calls).to.have.length(2);
});

it('can upload a new flavor for an existing version', async () => {
const baseUrl = 'https://example.com';
const token = 'FAKE_TOKEN';
const version = '2.0.0';
const flavor = 'lite';

// mock login
fetch.postOnce('path:/api/auth/login', { body: { token }, status: 200 });
// mock fetch all existing versions
fetch.getOnce('path:/api/version', { body: [{ name: '2.0.0', assets: [{ name: 'existing-artifact' }], flavor: 'default' }], status: 200 });
// mock creating a new version
fetch.postOnce('path:/api/version', { status: 200 });
// mock asset upload
fetch.post('path:/api/asset', { status: 200 });

const PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
'node-fetch': fetch,
}).default;

const publisher = new PublisherERS({
baseUrl,
username: 'test',
password: 'test',
flavor,
});

const makeResults: ForgeMakeResult[] = [
{
artifacts: ['/path/to/artifact'],
packageJSON: {
version,
},
platform: 'linux',
arch: 'x64',
},
];

await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ForgeConfig });

const calls = fetch.calls();

// creates a new version with the correct flavor, name, and channel
expect(calls[2][0]).to.equal(`${baseUrl}/api/version`);
expect(calls[2][1]?.body).to.equal(`{"channel":{"name":"stable"},"flavor":"${flavor}","name":"${version}","notes":""}`);

// uploads asset successfully
expect(calls[3][0]).to.equal(`${baseUrl}/api/asset`);
});

// TODO: implement edge cases
it('can read the channel from the package.json version');
it('does not upload the RELEASES file');
});

it('fails if username and password are not provided', () => {
const PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
'node-fetch': fetch,
}).default;

const publisher = new PublisherERS({});

expect(publisher.publish({ makeResults: [], dir: '', forgeConfig: {} as ForgeConfig })).to.eventually.be.rejectedWith(
'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'
);
});

it('fails if the server returns 4xx', async () => {
fetch.mock('begin:http://example.com', { body: {}, status: 400 });
const PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
'node-fetch': fetch,
Expand Down

0 comments on commit 6069ebe

Please sign in to comment.